Lua テクニカルノート7

モジュールとパッケージ

Roberto Ierusalimschy Roberto Ierusalimschy

概要

このノートでは、Luaでモジュール(パッケージとも呼ばれる)を実装する簡単な方法について説明します。提案する方法は、名前空間、プライバシー、およびその他の利点を提供します。

問題点

多くの言語は、Modulaのモジュール、JavaとPerlのパッケージ、C++の名前空間など、グローバル名前空間を整理するメカニズムを提供しています。これらのメカニズムにはそれぞれ、モジュール内で宣言された要素の使用に関する規則、可視性の規則、その他の詳細が異なります。しかし、いずれも異なるライブラリで定義された名前間の衝突を回避するための基本的なメカニズムを提供しています。各ライブラリは独自のネームスペースを作成し、このネームスペース内で定義された名前は他のネームスペースの名前と干渉しません。

Luaはパッケージのための明示的なメカニズムを提供していません。しかし、言語が提供する基本的なメカニズムを使用して、それらを簡単に実装できます。実際、それを行う方法はいくつかあり、それが問題を生み出します。Luaでパッケージを作成するための標準的な方法がないのです。さらに、ルールに従うかどうかはあなた次第であり、パッケージを実装するための固定された方法も、それらを操作するための固定された操作もありません。

解決策

パッケージをサポートしていない言語(Cなど)で使用される最初の解決策は、プレフィックスを選択し、パッケージ内のすべての名前のためにそのプレフィックスを使用することです。(Lua自体はそのように実装されています。すべての外部名は`lua`というプレフィックスで始まります。)その単純さにもかかわらず、これは非常に満足のいく解決策です(少なくとも、大規模プロジェクトでCを使用することを妨げるものではありませんでした)。

Luaでは、テーブルを使用してパッケージを実装する方が良い解決策です。グローバル変数としてではなく、識別子をテーブルのキーとして配置するだけです。ここで重要なのは、他の値と同様に、関数をテーブルの中に格納できることです。たとえば、複素数を操作するライブラリを作成するとします。各数を`r`(実数部)と`i`(虚数部)のフィールドを持つテーブルで表します。グローバル名前空間を汚染しないために、新しい操作をすべて新しいパッケージとして機能するテーブルに宣言します。

Complex = {}
Complex.i = {r=0, i=1}

function Complex.new (r, i) return {r=r, i=i} end

function Complex.add (c1, c2)
  return {r=c1.r+c2.r, i=c1.i+c2.i}
end

function Complex.sub (c1, c2)
  return {r=c1.r-c2.r, i=c1.i-c2.i}
end

function Complex.mul (c1, c2)
  return {r = c1.r*c2.r - c1.i*c2.i,
          i = c1.r*c2.i + c1.i*c2.r}
end

function Complex.inv (c)
  local n = c.r^2 + c.i^2
  return {r=c.r/n, i=c.i/n}
end

この定義により、次のように操作名を修飾して複素数演算を使用できます。

c = Complex.add(Complex.i, Complex.new(10, 20))

テーブルによるパッケージの使用は、実際のパッケージによって提供される機能と完全に同じではありません。Luaでは、すべての関数定義にパッケージ名を明示的に付ける必要があります。さらに、同じパッケージ内の別の関数を呼び出す関数は、呼び出される関数の名前を修飾する必要があります。パッケージの固定されたローカル名(たとえば`Public`)を使用し、このローカル名をパッケージの最終名に割り当てることで、これらの問題を改善できます。このガイドラインに従って、前の定義は次のように記述します。

local Public = {}
Complex = Public           -- package name

Public.i = {r=0, i=1}
function Public.new (r, i) return {r=r, i=i} end

...
関数が同じパッケージ内の別の関数を呼び出す場合(または再帰的に自身を呼び出す場合)、呼び出される関数にはパッケージのローカル名のアップ値を通してアクセスする必要があります。たとえば、
function Public.div (c1, c2)
  return %Public.mul(c1, %Public.inv(c2))
end
これらのガイドラインに従うと、2つの関数間の接続はパッケージ名に依存しなくなります。さらに、パッケージ名を書いているのはパッケージ全体で1箇所だけです。

プライバシー

通常、パッケージ内のすべての名前はエクスポートされます。つまり、パッケージのクライアントが使用できます。しかし、場合によっては、パッケージ自体だけが使用できるプライベート名、つまりパッケージ内部でのみ使用できる名前を持つことが役立ちます。これを行う便利な方法は、パッケージ内のプライベート名用に別のローカルテーブルを定義することです。このようにして、パッケージをパブリック名用のテーブルとプライベート名用のテーブルの2つのテーブルに分割します。パブリックテーブルをグローバル変数(パッケージ名)に割り当てているため、そのすべてのコンポーネントは外部からアクセスできます。しかし、プライベートテーブルをグローバル変数に割り当てていないため、パッケージ内部にロックされたままになります。この手法を説明するために、値が有効な複素数であるかどうかをチェックするプライベート関数を例に追加してみましょう。私たちの例は次のようになります。

local Public, Private = {}, {}
Complex = Public

function Private.checkComplex (c)
  assert((type(c) == "table") and tonumber(c.r) and tonumber(c.i),
         "bad complex number")
end

function Public.add (c1, c2)
  %Private.checkComplex(c1);
  %Private.checkComplex(c2);
  return {r=c1.r+c2.r, i=c1.i+c2.i}
end

...

では、このアプローチの長所と短所は何でしょうか?パッケージ内のすべての名前は、個別の名前空間に存在します。パッケージ内の各エンティティは、パブリックまたはプライベートとして明確にマークされています。さらに、真のプライバシーがあります。プライベートエンティティは、パッケージの外部からアクセスできません。このアプローチの主な欠点は、同じパッケージ内の他のエンティティにアクセスする場合の長文です。すべてのアクセスには接頭辞(`%Public.`または`%Private.`)が必要です。長文であるにもかかわらず、これらのアクセスは非常に効率的です。また、これらの2つの変数に対してより短いエイリアス(`local E, I = Public, Private`など)を提供することで、この長文を軽減できます。関数の状態をパブリックとプライベートの間で変更するたびに接頭辞を変更しなければならないという問題もあります。それにもかかわらず、私は全体としてこのアプローチが好きです。私にとって、負の側面(冗長さ)は、言語のシンプルさによって十分に補われています。結局のところ、言語から追加の機能を必要とすることなく、非常に満足のいくパッケージシステムを実装できます。

その他の機能

パッケージの実装にテーブルを使用することの明白な利点は、他のテーブルと同様にパッケージを操作し、Luaの全機能を使用して追加の機能を作成できることです。可能性は無限にあります。ここでは、いくつかの提案を示すにとどめます。

パッケージのパブリックアイテムをすべてまとめて定義する必要はありません。たとえば、別のチャンクで`Complex`パッケージに新しいアイテムを追加できます。

function Complex.div (c1, c2)
  return %Complex.mul(c1, %Complex.inv(c2))
end
(ただし、プライベート部分は1つのファイルに制限されていることに注意してください。これは良いことだと思います。)逆に、同じファイルに複数のパッケージを定義できます。行う必要があるのは、各パッケージを`do ... end`ブロックで囲み、その`Public`変数と`Private`変数がそのブロックに制限されるようにすることだけです。

いくつかの操作を頻繁に使用する場合、グローバル(またはローカル)名を与えることができます。

add = Complex.add
local i = Complex.i

c1 = add(Complex.new(10, 20), i)
または、パッケージ全体の名前を何度も書く必要がない場合は、パッケージ全体に短いローカル名を一度に付けることができます。
local C = Complex
c1 = C.add(C.new(10, 20), C.i)

パッケージ全体を開き、そのすべての名前をグローバル名前空間に配置する関数を簡単に記述できます。

function openpackage (ns)
  for n,v in ns do setglobal(n,v) end
end
openpackage(Complex)
c1 = mul(new(10, 20), i)
パッケージを開く際の名称衝突が心配な場合は、割り当ての前に名前を確認できます。
function openpackage (ns)
  for n,v in ns do
    if getglobal(n) ~= nil then
      error(format("name clash: `%s' is already defined", n))
    end
    setglobal(n,v)
  end
end

パッケージ自体はテーブルであるため、パッケージをネストすることもできます。つまり、別のパッケージの中にパッケージ全体を作成できます。ただし、そのような機能はめったに必要ありません。

通常、パッケージを作成する場合、そのコード全体を単一ファイルに配置します。次に、パッケージを開くまたはインポートする(つまり、使用可能にする)には、そのファイルを実行するだけです。たとえば、`complex.lua`というファイルに複素数パッケージの定義がある場合、`dofile("complex.lua")`というコマンドを実行すると、パッケージが開きます。パッケージが複数回ロードされた場合の無駄を避けるために、パッケージが既にロードされているかどうかをチェックしてパッケージを開始できます。

if Complex then return end

local Public, Private = {}, {}
Complex = Public

...
これで、`Complex`が既に定義されているときに`dofile("complex.lua")`を実行すると、ファイル全体がスキップされます。(注:Lua 4.1で使用可能になる新しい関数`require`により、このチェックは不要になります。)


最終更新日:2002年8月12日(月)15時51分10秒 EST