Lua 技術ノート 11

Require 再考: Import

作成者: Wim Couwenberg

このLTNはLua 5.0で導入された"loadfile"に依存します。

概要

Lua 4.1では、既にロードされていない場合にファイルをロードして実行する"require"関数が導入されました。Lua 5.0では、requireは基本ライブラリに組み込まれた関数として提供されています。requireコマンドは、LTN 7 "モジュールとパッケージ"と共に、Luaにおけるシンプルなモジュールサポートの基礎を提供します。この技術ノートでは、requireの改良版である"import"を提案します。提案されたimportスキームは、グローバル変数への直接アクセスを回避し、グローバル変数に関連するセキュリティ上の抜け穴を修正し、循環的なモジュール依存関係を適切に処理します。

問題点

LTN 7のモジュールアプローチでは、パッケージはパブリックインターフェース(テーブルにラップされている)を*グローバル変数テーブル*に公開することを提案しています。これには、以下の欠点があります。requireの現在の実装にもいくつかの欠点があります。

解決策

提案されたimportスキームは、*グローバルパッケージ名*、*グローバル変数のセキュリティ上の抜け穴*、*循環依存関係*によって引き起こされる問題に対処します。Importは、標準のLua 5で完全に実装できます。主なポイントは次のとおりです。

import関数は、次のLua 5.0コードで実装できます。

local imported = {}

local function package_stub(name)
  local stub = {}
  local stub_meta = {
    __index = function(_, index)
      error(string.format("member `%s' is accessed before package `%s' is fully imported", index, name))
    end,
    __newindex = function(_, index, _)
      error(string.format("member `%s' is assigned a value before package `%s' is fully imported", index, name))
    end,
  }
  setmetatable(stub, stub_meta)
  return stub
end

local function locate(name)
  local path = LUA_PATH
  if type(path) ~= "string" then
    path = os.getenv "LUA_PATH" or "./?.lua"
  end
  for path in string.gfind(path, "[^;]+") do
    path = string.gsub(path, "?", name)
    local chunk = loadfile(path)
    if chunk then return chunk, path end
  end
  return nil, path
end

function import(name)
  local package = imported[name]
  if package then return package end
  local chunk, path = locate(name)
  if not chunk then
    error(string.format("could not locate package `%s' in `%s'", name, path))
  end
  package = package_stub(name)
  imported[name] = package
  setglobals(chunk, getglobals(2))
  chunk = chunk()
  setmetatable(package, nil)
  if type(chunk) == "function" then
    chunk(package, name, path)
  end
  return package
end

importの典型的な使用法は次のとおりです。

-- import the complex package
local complex = import "complex"

-- complex now holds the public interface
local x = 5 + 3*complex.I

パッケージは次のように構成する必要があります。

-- first import all other required packages.
local a = import "a"
local b = import "b"

-- then define the package install function.
-- the PIF more or less contains the code of a
-- LTN 7 package.
local function pif(Public, path)

local Private = {}

function Public.fun()
  -- public function
end

-- etc.
end

-- return the package install function
return pif

説明

パッケージがロードされる直前に「パッケージスタブ」を設定すると、スタブへのアクセス(ネストされたインポートによって呼び出される)がトラップされる必要があります。これが機能するためには、追加のインポートは、通常は最初の呼び出しとして、関係する各パッケージの*グローバルスコープ*に配置する必要があります。スタブ(アクセス制限から削除されたもの)は、後でパッケージのパブリックインターフェースを保持することに注意してください。特に、インターフェースが実際にアクセスされない限り、循環依存関係であっても、インポートされたインターフェース(例えば、アップバリューを介して)を参照することは安全です。

Importは、requireとほぼ後方互換性があります。ただし、Importはロード中に_REQUIREDNAMEグローバルを定義しません。PIFを返さない「旧スタイル」のパッケージは、ロードされて実行されますが、importは空のパブリックインターフェースを返します。requireには戻り値がないため、これは旧スタイルのコードには影響しません。

これは、互いにインポートし合う2つのパッケージの例です。インポート中に実際にお互いを使用していないため、これは問題になりません。

パッケージ "a.lua"

local b = import "b"

local function pif(pub, name, path)

function pub.show()
  -- use a message from package b
  print("in " .. name .. ": " .. b.message)
end

pub.message = "this is package " .. name .. " at " .. path

end

return pif

パッケージ "b.lua"

local a = import "a"

local function pif(pub, name, path)

function pub.show()
  -- use a message from package a
  print("in " .. name .. ": " .. a.message)
end

pub.message = "this is package " .. name .. " at " .. path

end

return pif

そして、両方をインポートして実行するコード

local a = import "a"
local b = import "b"

a.show() -- prints "in a: this is package b at ./b.lua"
b.show() -- prints "in b: this is package a at ./a.lua"

弱点

import関数は、インポートするパッケージが「適切に動作する」と想定しています。もちろん、パッケージは引き続きグローバル変数にアクセスして更新できるため、注意が必要です。パッケージの適切な構造(グローバルスコープでのimport呼び出し、PIFの返却など)は強制されません。

結論

require関数は非常に便利であることが証明されています。提案されたimportスキームは、この成功に基づいています。より制御されたパッケージの可視性を提供し、可能な限り循環依存関係をサポートします。import機能は軽量であり、標準のLua 5で完全に定義できます。


最終更新日: 2003年2月19日水曜日 09:25:05 EST