Lua SPE論文

Software: Practice & Experience 26 #6 (1996) 635–652 からの再録。Copyright © 1996 John Wiley & Sons, Ltd. [ps · doi]

Lua – 拡張可能な拡張言語

著者:Roberto Ierusalimschy, Luiz Henrique de Figueiredo, Waldemar Celes Filho

概要

本論文では、アプリケーションを拡張するための言語であるLuaについて説明します。Luaは、手続き型機能と強力なデータ記述機能を、シンプルでありながら強力なテーブルのメカニズムを使用することで組み合わせています。このメカニズムは、レコード、配列、再帰的なデータ型(ポインタ)の概念を実装し、動的なディスパッチングを行うメソッドなどのオブジェクト指向機能を追加します。Luaは、プログラマーが言語のセマンティクスをいくつかの非従来的な方法で拡張できるフォールバックのメカニズムを提供します。特筆すべき例として、フォールバックを使用すると、ユーザーは言語に異なる種類の継承を追加できます。現在、Luaはユーザー構成、汎用データ入力、ユーザーインターフェースの記述、構造化されたグラフィカルメタファイルの保存、有限要素メッシュの汎用属性構成など、いくつかのタスクで本番環境で広く使用されています。

はじめに

カスタマイズ可能なアプリケーションに対する需要が増加しています。アプリケーションが複雑になるにつれて、単純なパラメーターによるカスタマイズは不可能になりました。ユーザーは実行時に構成を決定することを望み、生産性を向上させるためにマクロやスクリプトを作成することも望んでいます [1,2,3,4]。これらのニーズに対応して、現在では複雑なシステムをカーネル構成の2つの部分に分割する重要な傾向があります。カーネルはシステムの基本的なクラスとオブジェクトを実装し、通常はCやModula-2のようなコンパイルされた静的型付き言語で記述されます。通常はインタープリター型の柔軟な言語で記述される構成部分は、これらのクラスとオブジェクトを接続して、アプリケーションの最終的な形状を与えます [5]。

構成言語には、コマンドラインのパラメーターリストや構成ファイルから読み込まれる変数と値のペア(例:MS-Windowsの)として実装される、設定を選択するための単純な言語から、アプリケーションによって提供されるプリミティブに基づいてユーザー定義関数を使用してアプリケーションを拡張するための組み込み言語まで、いくつかの種類があります。組み込み言語は非常に強力であり、時にはLispやCのような主流のプログラミング言語を簡略化したものになることがあります。このような構成言語は、基本的なカーネルセマンティクスをユーザー定義の新しい機能で拡張できるため、拡張言語とも呼ばれます。.iniファイル、X11リソースファイル)、

拡張言語がスタンドアロン言語と異なるのは、ホストプログラムと呼ばれるホストクライアントに組み込まれて動作するということだけです。さらに、通常、ホストプログラムは、通常はより高いレベルの抽象化を提供することによって、組み込み言語を独自の目的のためにカスタマイズするためのドメイン固有の拡張機能を提供できます。このために、組み込み言語には、独自のプログラムの構文と、ホストと通信するためのアプリケーションプログラムインターフェース(API)の両方があります。ホストにパラメーター値とアクションシーケンスを提供する単純な構成言語とは異なり、組み込み言語とホストプログラムの間には双方向の通信があります。

拡張言語に対する要件は、汎用プログラミング言語に対する要件とは異なることに注意することが重要です。拡張言語の主な要件は次のとおりです。

本論文では、汎用の拡張言語として使用するように設計された、強力なデータ記述機能を備えた拡張可能な手続き型言語であるLuaについて説明します。Luaは、2つの特定のアプリケーションの構成用に設計された2つの記述言語の融合として生まれました。1つは科学データの入力用 [6]、もう1つは地質探査から得られた岩相プロファイルの可視化用です。ユーザーがこれらの言語でますます多くの機能を要求し始めたとき、実際のプログラミング機能が必要であることが明らかになりました。2つの異なる言語を並行してアップグレードおよび保守する代わりに、採用された解決策は、これら2つのアプリケーションだけでなく、他のアプリケーションにも使用できる単一の言語を設計することでした。したがって、Luaは、ほとんどの手続き型プログラミング言語に共通する機能(whileifなどの制御構造、代入、サブルーチン、およびインフィックス演算子)を組み込んでいますが、特定のドメインに固有の機能を抽象化しています。このようにして、Luaは完全な言語としてだけでなく、言語フレームワークとしても使用できます。

Luaは上記の要件を非常にうまく満たしています。その構文と制御構造は非常にシンプルで、Pascalに似ています。Luaは小さいです。ライブラリ全体は約6千行のANSI Cで構成されており、そのうち約2千行はyaccによって生成されています。最後に、Luaは拡張可能です。その設計では、多くの異なる機能の追加は、プログラマーがこれらの機能を自分で実装できるようにするいくつかのメタメカニズムの作成に置き換えられています。これらのメタメカニズムは、動的連想配列反射機能、およびフォールバックです。

動的連想配列は、通常の配列、レコード、セット、バッグなどの多数のデータ型を直接実装します。また、コンストラクターによって言語のデータ記述能力を活用します。

反射機能を使用すると、高度にポリモーフィックな部分を作成できます。永続性と複数の名前空間は、Luaに直接存在しない機能の例ですが、反射機能を使用してLua自体で簡単に実装できます。

最後に、Luaには固定の構文がありますが、フォールバックは多くの構文構造の意味を拡張できます。たとえば、フォールバックを使用して、Luaには存在しない機能である異なる種類の継承を実装できます。

Luaの概要

このセクションでは、Luaの主な概念について簡単に説明します。言語の雰囲気を味わうために、実際のコードの例をいくつか含めています。言語の完全な定義は、そのリファレンスマニュアル [7] にあります。

Luaは、データ記述機能を備えた手続き型プログラミングをサポートするように設計された汎用組み込みプログラミング言語です。Luaは組み込み言語であるため、「メイン」プログラムの概念はありません。ホストクライアントに組み込まれてのみ機能します。Luaは、ホストアプリケーションにリンクされるC関数のライブラリとして提供されます。ホストは、ライブラリ内の関数を呼び出してLuaでコードを実行し、Lua変数を書き込みおよび読み込み、Luaコードから呼び出されるC関数を登録できます。さらに、Luaがどのように進めるべきかわからない場合に呼び出されるフォールバックを指定できます。このようにして、Luaを拡張して、まったく異なるドメインに対処し、単一の構文フレームワークを共有するカスタマイズされたプログラミング言語を作成できます [8]。Luaが言語フレームワークであるのはこの意味です。一方、Lua用のインタラクティブなスタンドアロンインタープリターを記述するのは非常に簡単です(図1)。

      #include <stdio.h>
      #include "lua.h"              /* lua header file */
      #include "lualib.h"           /* extra libraries (optional) */

      int main (int argc, char *argv[])
      {
       char line[BUFSIZ];
       iolib_open();               /* opens I/O library (optional) */
       strlib_open();              /* opens string lib (optional) */
       mathlib_open();             /* opens math lib (optional) */
       while (gets(line) != 0)
         lua_dostring(line);
      }

図1:Luaのインタラクティブインタープリター。

Luaのすべてのステートメントは、すべてのグローバル変数と関数を保持するグローバル環境で実行されます。この環境はホストプログラムの開始時に初期化され、その終了まで持続します。

Luaの実行単位はチャンクと呼ばれます。チャンクにはステートメントと関数の定義を含めることができます。チャンクが実行されると、最初にすべての関数とステートメントがコンパイルされ、関数がグローバル環境に追加されます。次に、ステートメントが順番に実行されます。

図2は、Luaを非常に単純な構成言語として使用する方法の例を示しています。このコードは3つのグローバル変数を定義し、それらに値を代入します。Luaは動的型付け言語です。変数には型がなく、値にのみ型があります。すべての値は独自の型を持ちます。したがって、Luaには型定義はありません。

      width = 420
      height = width*3/2     -- ensures 3/2 aspect ratio
      color = "blue"

図2:非常に単純な構成ファイル。

フロー制御と関数定義を使用して、より強力な構成を記述できます。Luaは、予約語と明示的に終了されたブロックを備えた、従来のPascalのような構文を使用します。セミコロンはオプションです。このような構文は、馴染み深く、堅牢で、解析が簡単です。図3に簡単な例を示します。関数は複数の値を返すことができ、これらの値を収集するために複数の代入を使用できることに注意してください。したがって、常に小さな意味的な難しさの源である参照によるパラメーター渡しは、言語から破棄できます。

      function Bound (w, h)
        if w < 20 then w = 20
        elseif w > 500 then w = 500
        end
        local minH = w*3/2             -- local variable
        if h < minH then h = minH end
        return w, h
      end

      width, height = Bound(420, 500)
      if monochrome then color = "black" else color = "blue" end

図3:関数を使用した構成ファイル。

Luaの関数はファーストクラスの値です。関数の定義は、function型の値を作成し、この値をグローバル変数(図3のBound)に代入します。他の任意の値と同様に、関数の値は変数に格納したり、他の関数への引数として渡したり、結果として返したりできます。この機能により、このセクションで後述するように、オブジェクト指向機能の実装が大幅に簡素化されます。

Luaは、基本的な型number(浮動小数点数)とstring、および型functionに加えて、他の3つのデータ型(niluserdata、およびtable)を提供します。明示的な型チェックが必要な場合は、プリミティブ関数typeを使用できます。この関数は、引数の型を記述する文字列を返します。

nilには、他の任意の値とは異なることが主な特性である、nilという単一の値があります。最初の代入の前は、変数の値はnilです。したがって、プログラムエラーの主な原因である未初期化の変数は、Luaには存在しません。実際の値が必要なコンテキスト(たとえば、算術式)でnilを使用すると、実行エラーが発生し、プログラマーに変数が適切に初期化されなかったことを警告します。

userdataは、void* Cポインターとして表される任意のホストデータをLua変数に格納できるようにするために提供されます。この型の値に対して有効な操作は、代入と等価テストのみです。

最後に、型tableは連想配列を実装します。つまり、整数だけでなく、文字列、実数、テーブル、および関数の値でインデックスを付けることができる配列です。

連想配列

連想配列は強力な言語構造です。多くのアルゴリズムは、必要なデータ構造とそれらを検索するためのアルゴリズムが言語によって暗黙的に提供されるため、自明なほどに単純化されます [9]。通常の配列、セット、バッグ、シンボルテーブルのような、ほとんどの一般的なデータコンテナは、テーブルによって直接実装できます。テーブルは、フィールド名をインデックスとして使用することで、レコードをシミュレートすることもできます。Lua は、a.namea["name"] のシンタックスシュガーとして提供することで、この表現をサポートしています。

AWK [10]、Tcl [11]、Perl [12] などの連想配列を実装する他の言語とは異なり、Lua のテーブルは変数名にバインドされていません。代わりに、従来の言語のポインタのように操作できる動的に作成されるオブジェクトです。この選択の欠点は、テーブルを使用する前に明示的に作成する必要があることです。利点は、テーブルが他のテーブルを自由に参照できるため、再帰的なデータ型をモデル化したり、場合によってはサイクルを持つ一般的なグラフ構造を作成したりするための表現力が高いことです。例として、図 4 は、Lua で循環リンク リストを構築する方法を示しています。

      list = {}                    -- creates an empty table
      current = list
      i = 0
      while i < 10 do
        current.value = i
        current.next = {}
        current = current.next
        i = i+1
      end
      current.value = i
      current.next = list

図 4: Lua での循環リンク リスト。

Lua には、テーブルを作成するための多くの興味深い方法が用意されています。最も単純な形式は式 {} で、これは新しい空のテーブルを返します。テーブルを作成し、いくつかのフィールドを初期化するより記述的な方法を以下に示します。構文は BibTeX [13] データベース形式に多少影響を受けています。

      window1 = {x = 200, y = 300, foreground = "blue"}

このコマンドはテーブルを作成し、そのフィールド xyforeground を初期化して、変数 window1 に割り当てます。テーブルは均質である必要はなく、すべての型の値を同時に格納できることに注意してください。

同様の構文を使用してリストを作成できます。

      colors = {"blue", "yellow", "red", "green", "black"}

このステートメントは次のものと同等です。

      colors = {}
      colors[1] = "blue";  colors[2] = "yellow"; colors[3] = "red"
      colors[4] = "green"; colors[5] = "black"

場合によっては、より強力な構成機能が必要になることがあります。すべてを提供しようとするのではなく、Lua は単純なコンストラクタメカニズムを提供します。コンストラクタは name{...} と記述され、これは単に name({...}) のシンタックスシュガーです。したがって、コンストラクタを使用すると、テーブルが作成、初期化され、関数へのパラメータとして渡されます。この関数は、(動的な)型チェック、欠落しているフィールドの初期化、およびホストプログラムでの補助データ構造の更新など、必要な初期化を行うことができます。通常、コンストラクタ関数は C または Lua で事前定義されており、構成ユーザーはコンストラクタが関数であることを認識していないことがよくあります。彼らは単に次のようなものを記述します。

      window1 = Window{ x = 200, y = 300, foreground = "blue" }

そして、「ウィンドウ」やその他の高レベルの抽象化について考えます。したがって、Lua は動的に型付けされますが、ユーザーが制御できる型コンストラクタを提供します。

コンストラクタは式であるため、以下のコードのように、宣言的なスタイルでより複雑な構造を記述するためにネストできます。

      d = dialog{
                 hbox{
                      button{ label = "ok" },
                      button{ label = "cancel" }
                 }
          }

リフレクティブ機能

Lua のもう 1 つの強力なメカニズムは、組み込み関数 next を使用してテーブルをトラバースする機能です。この関数は、トラバースするテーブルとこのテーブルのインデックスという 2 つの引数を取ります。インデックスが nil の場合、関数は指定されたテーブルの最初のインデックスとこのインデックスに関連付けられた値を返します。インデックスが nil でない場合、関数は次のインデックスとその値を返します。インデックスは任意の順序で取得され、トラバースの終了を知らせるために nil インデックスが返されます。Lua のトラバース機能の使用例として、図 5 はオブジェクトを複製するためのルーチンを示しています。ローカル変数 i はオブジェクト o のインデックスを反復処理し、v はそれらの値を受け取ります。これらの値は、対応するインデックスに関連付けられ、ローカルテーブル new_o に格納されます。

   function clone (o)
     local new_o = {}           -- creates a new object
     local i, v = next(o,nil)   -- get first index of "o" and its value
     while i do
       new_o[i] = v             -- store them in new table
       i, v = next(o,i)         -- get next index and its value
     end
     return new_o
   end

図 5: ジェネリックオブジェクトを複製する関数。

next がテーブルをトラバースするのと同じように、関連する関数 nextvar は Lua のグローバル変数をトラバースします。図 6 は、Lua のグローバル環境をテーブルに保存する関数を示しています。関数 clone のように、ローカル変数 n はすべてのグローバル変数の名前を反復処理し、v はそれらの値を受け取り、ローカルテーブル env に格納されます。終了時に、関数 save はこのテーブルを返し、後で関数 restore に渡して環境を復元できます(図 7)。この関数には 2 つのフェーズがあります。まず、事前定義された関数を含む現在の環境全体が消去されます。次に、ローカル変数 nv は、指定されたテーブルのインデックスと値を反復処理し、これらの値を対応するグローバル変数に格納します。注意が必要な点は、restore によって呼び出される関数は、すべてのグローバル名が消去されるため、ローカル変数に保持する必要があることです。

   function save ()
     local env = {}             -- create a new table
     local n, v = nextvar(nil)  -- get first global var and its value
     while n do
       env[n] = v               -- store global variable in table
       n, v = nextvar(n)        -- get next global var and its value
     end
     return env
   end

図 6: Lua 環境を保存する関数。

   function restore (env)
     -- save some built-in functions before erasing global environment
     local nextvar, next, setglobal = nextvar, next, setglobal
     -- erase all global variables
     local n, v = nextvar(nil)
     while n do
       setglobal(n, nil)
       n, v = nextvar(n)
     end
     -- restore old values
     n, v = next(env, nil)      -- get first index; v = env[n]
     while n do
      setglobal(n, v)           -- set global variable with name n
      n, v = next(env, n)
     end
   end

図 7: Lua 環境を復元する関数。

興味深い例ではありますが、オブジェクトとして使用されるテーブルは複数の環境を維持するためのより良い方法を提供するため、Lua でのグローバル環境の操作はほとんど必要ありません。

オブジェクト指向プログラミングのサポート

関数はファーストクラスの値であるため、テーブルフィールドは関数を参照できます。このプロパティにより、いくつかの興味深いオブジェクト指向機能の実装が可能になります。この機能は、メソッドを定義および呼び出すためのシンタックスシュガーによって容易になります。

まず、メソッド定義は次のように記述できます。

      function object:method (params)
        ...
      end

これは次のものと同等です。

      function dummy_name (self, params)
        ...
      end
      object.method = dummy_name

つまり、匿名関数が作成され、テーブルフィールドに格納されます。さらに、この関数には self という非表示のパラメータがあります。

次に、メソッド呼び出しは次のように記述できます。

      receiver:method(params)

これは次のように変換されます。

      receiver.method(receiver,params)

つまり、メソッドの受信者は最初の引数として渡され、パラメータ self に予期される意味を与えます。

上記の構造のいくつかの特性に注意する価値があります。まず、情報隠蔽を提供しません。したがって、純粋主義者は、オブジェクト指向の重要な部分が欠落していると(正当に)主張する可能性があります。次に、クラスを提供しません。各オブジェクトは独自の操作を実行します。それにもかかわらず、この構造は非常に軽量(シンタックスシュガーのみ)であり、Self [14] のような他のプロトタイプベースの言語で一般的なように、継承を使用してクラスをシミュレートできます。ただし、継承について説明する前に、フォールバックについて説明する必要があります。

フォールバック

型付けされていない言語である Lua には、多くの実行時異常条件を持つセマンティクスがあります。例としては、数値以外のオペランドに適用される算術演算、テーブル以外の値をインデックス化しようとする試み、または関数以外の値を呼び出そうとする試みがあります。これらの状況で停止することは埋め込み言語には不適切であるため、Lua では、プログラマーがエラー状態を処理するために独自の関数を設定できます。このような関数はフォールバック関数と呼ばれます。フォールバックは、テーブル内の欠落しているフィールドへのアクセスやガベージコレクションの通知など、厳密にはエラー状態ではない他の状況を処理するためのフックを提供するためにも使用されます。

フォールバック関数を設定するには、プログラマーは関数 setfallback を、フォールバックを識別する文字列と、対応する条件が発生するたびに呼び出す新しい関数という 2 つの引数で呼び出します。関数 setfallback は古いフォールバック関数を返すため、プログラムはさまざまな種類のオブジェクトに対してフォールバックをチェーンできます。

Lua は、次の文字列で識別される次のフォールバックをサポートします。

"arith"、"order"、"concat"
これらのフォールバックは、無効なオペランドに演算が適用されたときに呼び出されます。これらは、2 つのオペランドと、違反した演算子("add""sub"、...)を記述する文字列という 3 つの引数を受け取ります。それらの戻り値は、演算の最終的な結果です。これらのフォールバックのデフォルト関数はエラーを発行します。
"index"
このフォールバックは、Lua がテーブルに存在しないインデックスの値を取得しようとしたときに呼び出されます。テーブルとインデックスが引数として渡されます。その戻り値は、インデックス操作の最終的な結果です。デフォルト関数は nil を返します。
"gettable"、"settable"
Lua がテーブル以外の値のインデックスの値を読み書きしようとしたときに呼び出されます。デフォルト関数はエラーを発行します。
"function"
Lua が関数以外の値を呼び出そうとしたときに呼び出されます。関数以外の値と、元の呼び出しで指定された引数が引数として渡されます。その戻り値は、呼び出し操作の最終的な結果です。デフォルト関数はエラーを発行します。
"gc"
ガベージコレクション中に呼び出されます。ガベージコレクションされるテーブルが引数として渡され、ガベージコレクションの終了を知らせるために nil が渡されます。デフォルト関数は何も行いません。

先に進む前に、フォールバックは通常、通常の Lua プログラマーによって設定されないことに注意することが重要です。フォールバックは、Lua を特定のアプリケーションにバインドするときに、主にエキスパートプログラマーによって使用されます。その後、この機能は言語の不可欠な部分として使用されます。典型的な例として、ほとんどの実際のアプリケーションでは、以下で説明するように、継承を実装するためにフォールバックを使用しますが、ほとんどの Lua プログラマーは、その実装方法を知らなくても(または気にしなくても)継承を使用しています。

フォールバックの使用

図 8 は、フォールバックを使用して、よりオブジェクト指向のバイナリ演算子の解釈スタイルを可能にする例を示しています。このフォールバックが設定されている場合、a+b のような式(a がテーブルの場合)は a:add(b) として実行されます。フォールバック関数をチェーンするためにグローバル変数 oldFallback が使用されていることに注意してください。

      function dispatch (receiver, parameter, operator)
        if type(receiver) == "table" then
          return receiver[operator](receiver, parameter)
        else
          return oldFallback(receiver, parameter, operator)
        end
      end

      oldFallback = setfallback("arith", dispatch)

図 8: フォールバックの例。

フォールバックによって提供されるもう 1 つの珍しい機能は、Lua のパーサーの再利用です。多くのアプリケーションは算術式パーサーから恩恵を受けるでしょうが、誰もが必要な専門知識を持っているわけでも、スクラッチからパーサーを作成したり、yacc などのパーサージェネレーターを使用したりする意欲がないため、パーサーを含めません。図 9 は、フォールバックを使用した式パーサーの完全な実装を示しています。このプログラムは、変数 a、...、z の算術式を読み取り、式を評価するために必要な一連のプリミティブ操作を、一時変数として変数 t1t2、... を使用して出力します。たとえば、式に対して生成されたコード

      (a*a+b*b)*(a*a-b*b)/(a*a+b*b+c)+(a*(b*b)*c)

      t1=mul(a,a)      t2=mul(b,b)      t3=add(t1,t2)
      t4=sub(t1,t2)    t5=mul(t3,t4)    t6=add(t3,c)
      t7=div(t5,t6)    t8=mul(a,t2)     t9=mul(t8,c)
      t10=add(t7,t9)

このプログラムの主な部分は、算術演算のフォールバックとして設定されている関数 arithfb です。関数 create は、変数名を含むフィールド name を持つテーブルで変数 a、...、z を初期化するために使用されます。この初期化後、ループは算術式を含む行を読み取り、変数 E への代入を構築して、dostring を呼び出す Lua インタープリターに渡します。インタープリターが a*a のようなコードを実行しようとするたびに、a の値が数値ではなくテーブルであるため、"arith" フォールバックを呼び出します。フォールバックは、各プリミティブ算術演算の結果のシンボリック表現を格納するための一時変数を作成します。

このコードは小さいながらも、グローバルな共通部分式を識別し、最適化されたコードを生成します。上記の例では、a*a+b*ba*a-b*bの両方が、a*ab*bの単一の評価に基づいて評価されていることに注目してください。また、a*a+b*bは一度だけ評価されることにも注目してください。コードの最適化は、以前に計算された量を、プリミティブ演算のテキスト表現でインデックス付けされたテーブルTにキャッシュすることで簡単に行われます。その値は、結果を含む一時変数です。たとえば、T["mul(a,a)"]の値はt1です。

図9のコードは、加算と乗算の可換性、減算と除算の反可換性を処理するように簡単に変更できます。また、後置表現やその他の形式を出力するように変更することも簡単です。

実際のアプリケーションでは、変数a、...、zは、複素数、行列、さらには画像などのアプリケーションオブジェクトを表し、"arith"フォールバックは、これらのオブジェクトに対して実際の計算を実行するためにアプリケーション関数を呼び出します。したがって、Luaパーサーの主な用途は、プログラマーが使い慣れた算術式を使用して、アプリケーションオブジェクトに対する複雑な計算を表現できるようにすることです。

      n=0                            -- counter of temporary variables
      T={}                           -- table of temporary variables

      function arithfb(a,b,op)
       local i=op .. "(" .. a.name .. "," .. b.name .. ")"
       if T[i]==nil then             -- expression not seen yet
         n=n+1
         T[i]=create("t"..n)         -- save result in cache
         print(T[i].name ..'='..i)
       end
       return T[i]
      end

      setfallback("arith",arithfb)   -- set arithmetic fallback

      function create(v)             -- create symbolic variable
       local t={name=v}
       setglobal(v,t)
       return t
      end

      create("a") create("b") create("c") ... create("z")

      while 1 do                     -- read expressions
       local s=read()
       if (s==nil) then exit() end
       dostring("E="..s)             -- execute fake assignment
       print(s.."="..E.name.."\n")
      end

図9:Luaにおける最適化された算術式コンパイラー。

フォールバックによる継承

確かに、フォールバックの最も興味深い用途の1つは、Luaでの継承の実装です。単純な継承では、オブジェクトは、と呼ばれる別のオブジェクトで、存在しないフィールドの値を検索できます。特に、このフィールドはメソッドである可能性があります。このメカニズムは、SmalltalkやC++で採用されているより伝統的なクラス継承とは対照的に、一種のオブジェクト継承です。Luaで単純な継承を実装する1つの方法は、親オブジェクトをparentという名前の区別されたフィールドに格納し、図10に示すようにインデックスフォールバック関数を設定することです。このコードは関数Inheritを定義し、それを"index"フォールバックとして設定します。Luaがオブジェクトに存在しないフィールドにアクセスしようとするたびに、フォールバックメカニズムは関数Inheritを呼び出します。この関数は最初に、オブジェクトにテーブル値を含むフィールドparentがあるかどうかを確認します。もしそうなら、この親オブジェクトで目的のフィールドにアクセスしようとします。このフィールドが親に存在しない場合、フォールバックは自動的に再度呼び出されます。このプロセスは、フィールドの値が見つかるか、親チェーンが終了するまで「上向き」に繰り返されます。

      function Inherit (object, field)
        if field == "parent" then     -- avoid loops
          return nil
        end
        local p = object.parent       -- access parent object
        if type(p) == "table" then    -- check if parent is a table
          return p[field]             -- (this may call Inherit again)
        else
          return nil
        end
      end

      setfallback("index", Inherit)

図10:Luaでの単純な継承の実装。

上記のスキームでは、無限のバリエーションが可能です。たとえば、メソッドのみを継承したり、アンダースコアで始まるフィールドのみを継承したりすることができます。多重継承の多くの形式も実装できます。その中でも、頻繁に使用される形式は二重継承です。このモデルでは、親階層でフィールドが見つからない場合は常に、検索は通常"godparent"と呼ばれる別の親を通じて続行されます。ほとんどの場合、追加の親が1つあれば十分です。さらに、二重継承は汎用的な多重継承をモデル化できます。たとえば、以下のコードでは、aa1a2、およびa3からこの順序で継承します。

      a = {parent = a1, godparent = {parent = a2, godparent = a3}}

実際のアプリケーションでのLuaの使用

TeCGrafは、リオデジャネイロのポンティフィカルカトリック大学(PUC-Rio)にある研究開発研究所で、多くの産業パートナーがいます。TeCGrafの約40人のプログラマーが、過去2年間にLuaを使用して、いくつかの重要な製品を開発しました。このセクションでは、これらの使用例のいくつかについて説明します。

岩相プロファイル用の設定可能なレポートジェネレーター

はじめにで述べたように、Luaは当初、独自のものだが限定された拡張言語を持つ2つの異なるアプリケーションをサポートするために登場しました。これらのアプリケーションの1つは、地質探査から得られた岩相プロファイルを視覚化するためのツールです。その主な特徴は、オブジェクトのインスタンスを組み合わせて、表示するデータを指定することにより、プロファイルのレイアウトをユーザーが設定できるようにすることです。このプログラムは、連続曲線、ヒストグラム、岩相表現、スケールなど、いくつかの種類のオブジェクトをサポートしています。

レイアウトを構築するために、ユーザーはこれらのオブジェクトを記述するLuaコードを記述できます(図11)。アプリケーション自体にも、グラフィカルユーザーインターフェイスを使用してそのような記述を作成できるLuaコードがあります。この機能は、以下で説明するEDGフレームワーク上に構築されました。

      Grid{
        name = "log",
        log = TRUE,
        h_step = 25,
        v_step = 25,
        v_tick = 5,
        step_line = Line {color = RED, width = SIMPLE},
        tick_line = Line {color = CORAL}
      }

図11:Luaでの岩相プロファイルオブジェクトの記述。

構造化されたグラフィカルメタファイルの保存

Luaのもう1つの重要な用途は、構造化されたグラフィカルメタファイルの保存です。TeCGrafによって開発された汎用描画エディターTeCDrawは、描画を構成するグラフィックオブジェクトの高度な記述をLuaで含むメタファイルを保存します。図12は、これらの記述を示しています。

      line{
         x = { 0.0, 1.0 },
         y = { 5.0, 8.0 },
         color = RED
      }
      text{
         x = 0.8,
         y = 0.5,
         text = 'an example of text',
         color = BLUE
      }
      circle{
         x = 1.0,
         y = 1.0,
         r = 5.0
      }

図12:構造化されたグラフィカルメタファイルの抜粋。

このような汎用的な構造化メタファイルは、開発にいくつかの利点をもたらします

高度な汎用グラフィカルデータ入力

Luaの機能は、高度な抽象化レベルでデータ入力プログラムの開発をサポートするためのシステムであるEDGの実装でも大いに活用されています。このシステムは、インターフェイスオブジェクト(ボタン、メニュー、リストなど)とグラフィックオブジェクト(線、円、プリミティブのグループなど)の操作を提供します。したがって、プログラマーは、高度な抽象化プログラミングレベルで洗練されたインターフェイスダイアログを構築できます。プログラマーは、コールバックアクションをグラフィックオブジェクトに関連付けることもできるため、ユーザー入力に手続き的に反応するアクティブなオブジェクトを作成できます。

EDGシステムは、上記で説明したように、二重継承を実装するためにLuaフォールバック機能を使用します。したがって、元のオブジェクトの動作を継承して、新しいインターフェイスオブジェクトとグラフィックオブジェクトを構築できます。EDGに存在する継承のもう1つの興味深い用途は、クロス言語継承です。EDGは、移植可能なユーザーインターフェイストゥールキットIUP [15]上に構築されています。ホストにあるIUPデータをLuaで重複させるのを避けるために、EDGは"gettable""settable"のフォールバックを使用して、Luaから直接ツールキットのフィールドにアクセスします。したがって、ホストの各エクスポートされたデータ項目にアクセス関数を作成することなく、直感的なレコード構文を使用して、ホストデータに直接アクセスできます。

EDGシステムは、いくつかのデータ入力プログラムの開発で使用されてきました。多くのエンジニアリングシステムでは、完全な分析は3つのステップに分かれています。データ入力(前処理と呼ばれます)、分析自体(処理またはシミュレーションと呼ばれます)、および結果のレポートと検証(後処理と呼ばれます)。データ入力タスクは、分析への入力として指定する必要があるデータのグラフィカル表現を描画することで簡単になります。このようなアプリケーションでは、EDGシステムは非常に役立ち、カスタマイズされたデータ入力のための高速開発ツールを提供します。これらのグラフィカルデータ入力ツールは、バッチシミュレーションプログラムのレガシーコードに新しい命を吹き込みました。

有限要素メッシュの汎用属性構成

Luaが使用されているもう1つのエンジニアリング分野は、有限要素メッシュの生成です。有限要素メッシュは、解析ドメインを分解するノードと要素で構成されています。モデルを完成させるために、材料タイプ、サポート条件、荷重ケースなどの物理的特性(属性)をノードと要素に関連付ける必要があります。指定する必要がある属性のセットは、実行する分析によって大きく異なります。したがって、汎用性の高い有限要素メッシュジェネレーターを実装するには、属性をユーザーが設定可能にし、プログラムにハードコードしないことをお勧めします。

ESAM [16]は、Luaを使用して属性構成をサポートする汎用システムです。EDGと同様に、ESAMはオブジェクト指向のアプローチを採用しています。ユーザーは、事前定義されたコアクラスから派生した特定のプロパティを作成します。図13は、「等方性」と呼ばれる新しい種類の材料を作成する方法の例を示しています。

      ISO_MAT = ctrclass{ parent = MATERIAL,
                           name = "Isotropic",
                           vars = {"e", "nu"}
                }

      function ISO_MAT:CrtDlg ()
        ...  -- creates a dialog to specify this material
      end

図13:ESAMでの新しい材料の作成。

関連研究

このセクションでは、他のいくつかの拡張言語について説明し、それらをLuaと比較します。包括的にすることはありません。代わりに、拡張言語の現在のトレンドのいくつかの代表例が選択されています。Scheme、Tcl、Pythonです。組み込み言語の包括的なリストは、インターネットで入手できます [17]。このセクションでは、フォールバックメカニズムと他のいくつかの言語メカニズムも比較します。

Lisp方言、特にSchemeは、その単純で簡単に解析できる構文と組み込みの拡張性から、拡張言語として常に人気のある選択肢でした [8,18,19]。たとえば、テキストエディターEmacsの主要な部分は、実際には独自のLispの変種で記述されています。他のいくつかのテキストエディターも同じ道をたどってきました。現在、特に組み込み言語として使用されるように設計されたライブラリの形で、Schemeの多くの実装があります(たとえば、libscheme [18]、OScheme [20]、およびElk [3])。ただし、Lispはカスタマイズに関してはユーザーフレンドリーとは言えません。その構文は、プログラマー以外にとってはかなり粗末です。さらに、LispまたはSchemeの真に移植可能な実装はほとんどありません。

最近、非常に人気のある拡張言語はTcl [11]です。間違いなく、その成功の理由の1つは、グラフィカルユーザーインターフェイスを構築するための強力なTclツールキットであるTkの存在です。Tclは非常にプリミティブな構文を持っており、それによってインタプリタが大幅に簡素化されますが、わずかに複雑な構造を記述するのも複雑になります。たとえば、変数Aの値を2倍にするTclコードはset A [expr $A*2]です。Tclは単一のプリミティブ型、文字列をサポートしています。この事実に加えて、プリコンパイルがないため、Tclは拡張言語の場合でもかなり非効率的です。これらの問題を修正することで、TC [21]で示されているように、Tclの効率を5〜10倍向上させることができます。Luaは、より適切なデータ型とプリコンパイルにより、Tclよりも10〜20倍高速に実行されます。簡単なテストでは、Sparcstation 1で実行されているTcl 7.3での引数なしの手続き呼び出しのコストは約44μsで、グローバル変数のインクリメントには76μsかかります。Lua v. 2.1では、同じ操作のコストはそれぞれ6μsと4μsです。一方、LuaはCよりも約20倍遅いです。これは、インタプリタ型言語の一般的な値のようです [22]。

Tclには、whileifなどの組み込み制御構造はありません。代わりに、Smalltalkのように、遅延評価を介して制御構造をプログラムできます。強力でエレガントですが、プログラム可能な制御構造は非常に暗号的なプログラムにつながる可能性があり、実際にはめったに使用されません。さらに、多くの場合、パフォーマンスが大幅に低下します。

Python [23]は、拡張言語としても提案されている興味深い新しい言語です。ただし、作者自身によると、「Pythonを他のアプリケーションに埋め込むためのサポートの改善、たとえば、ほとんどのグローバルシンボルを`Py`プレフィックスを持つように名前を変更する」必要がまだあります [24]。Pythonは小さな言語ではなく、モジュールや例外処理など、拡張言語では必要ない多くの機能があります。これらの機能は、言語を使用するアプリケーションに追加のコストを追加します。

Luaは、拡張可能な拡張言語としての目的を達成するために、既存の言語の長所を組み合わせるように設計されています。Tclと同様に、LuaはCへのシンプルなインターフェースを持つ小さなライブラリです。このインターフェースは、100行の単一のヘッダーファイルです。しかし、Tclとは異なり、Luaは標準のバイトコード中間形式にプリコンパイルされます。Pythonと同様に、Luaはクリーンで使い慣れた構文を持ち、オブジェクトの概念が組み込まれています。Lispと同様に、Luaは単一のデータ構造メカニズム(テーブル)を持ち、ほとんどのデータ構造を効率的に実装するのに十分強力です。テーブルはハッシュを使用して実装されます。衝突は線形プロービングによって処理され、テーブルが70%以上満たされると自動的に再割り当てと再ハッシュが行われます。ハッシュ値はアクセスパフォーマンスを向上させるためにキャッシュされます。

Luaで提供されるフォールバックメカニズムは、復帰を伴う一種の例外処理メカニズムと見なすことができます[25]。しかし、Luaの動的な性質により、静的に型付けされた言語であればコンパイル時にエラーが発生するような場合でも、多くの場合に使用できます。上記で示した両方の例がこの種です。特に、"arith""order""concat"の3つのフォールバックは、主にオーバーロードを実装するために使用されます。特に、図9の例は、AdaやC++のようにオーバーロードを備えた他の言語に容易に翻訳できます。ただし、その動的な性質のために、フォールバックは例外処理やオーバーロードメカニズムよりも柔軟性があります。一方、一部の著者[26]は、これらのメカニズムを使用するプログラムは、検証、理解、デバッグが困難になる傾向があると主張しています。これらの困難は、フォールバックを使用すると悪化します。フォールバックは、注意と節度を持って、熟練したプログラマーのみが記述する必要があります。

結論

構成アプリケーションへの需要の増加は、プログラムの構造を変化させています。今日、多くのプログラムは2つの異なる言語で記述されています。1つは強力な「仮想マシン」を記述するための言語で、もう1つはこのマシン用の単一のプログラムを記述するための言語です。Luaは、後者のタスクのために特別に設計された言語です。それは小さいです。すでに述べたように、ライブラリ全体はANSI Cの約6,000行です。それは移植性があります。LuaはPC-DOSからCRAYまでのプラットフォームで使用されています。シンプルな構文とシンプルな意味論を持っています。そして、柔軟性があります。

そのような柔軟性は、言語を高度に拡張可能にするいくつかの珍しいメカニズムによって達成されています。これらのメカニズムの中で、以下を強調します。

連想配列は強力な統一的なデータコンストラクタです。さらに、文字列やリストなどの他の統一的なコンストラクタよりも効率的なアルゴリズムを可能にします。連想配列を実装する他の言語[10,11,12]とは異なり、Luaのテーブルは動的に作成されるオブジェクトで、アイデンティティを持ちます。これにより、テーブルをオブジェクトとして使用したり、オブジェクト指向の機能を追加したりすることが大幅に簡素化されます。

フォールバックにより、プログラマーはほとんどの組み込み操作の意味を拡張できます。特に、インデックス操作のフォールバックを使用すると、さまざまな種類の継承を言語に追加できます。一方、"arith"およびその他の演算子のフォールバックは、動的なオーバーロードを実装できます。

データ構造トラバーサルのための反射機能は、高度にポリモーフィックなコードの生成に役立ちます。他のシステムでプリミティブとして提供する必要がある、または新しいタイプごとに個別にコーディングする必要がある多くの操作は、Luaでは単一の汎用形式でプログラミングできます。例としては、オブジェクトのクローン作成やグローバル環境の操作などがあります。

いくつかの産業アプリケーションでLuaを使用することに加えて、現在、Luaコードを含むメッセージを相互に送信する分散オブジェクトを使用した計算[27](以前にTclで提案されたアイデア[4])から、クライアント側のLuaコードでWWWブラウザを透過的に拡張することまで、多くの研究プロジェクトでLuaを実験しています。Luaとオペレーティングシステムをインターフェースするすべての関数は外部ライブラリで提供されるため、適切なセキュリティを提供するためにインタープリターの機能を制限するのは簡単です。

また、Luaのデバッグ機能を改善する予定です。現在、利用できるのはシンプルなスタックトレースバックのみです。プログラマーが独自の拡張機能を構築できるようにする強力なメタメカニズムを提供するという哲学に従い、関数に入ったり、終了したり、ユーザーコードの行を実行したりするなど、重要なイベントが発生したときにユーザープログラムに通知できるように、ランタイムシステムにシンプルなフックを追加する予定です。これらの基本的なフックに基づいて、さまざまなデバッグインターフェースを構築できます。さらに、フックはパフォーマンス分析のためのプロファイラなどの他のツールを構築するためにも役立ちます。

この論文で説明されているLuaの実装は、インターネットで入手できます。

      https://lua.dokyumento.jp/ftp/lua-2.1.tar.gz

謝辞

Luaを使用およびテストしてくれたICADおよびTeCGrafのスタッフ、およびLuaの以前のバージョンのフォールバックに関する貴重な提案をメールでしてくれたJohn Rollに感謝します。本文で言及した産業アプリケーションは、PETROBRAS(ブラジル石油会社)およびELETROBRAS(ブラジル電力会社)の研究センターとのパートナーシップで開発されています。著者は、ブラジル政府(CNPqおよびCAPES)からの研究開発助成金によって部分的に支援されています。Luaはポルトガル語でを意味します。

参考文献

[1] B. Ryan, "Scripts unbounded", Byte, 15(8), 235–240 (1990).

[2] N. Franks, "Adding an extension language to your software", Dr. Dobb's Journal, 16(9), 34–43 (1991).

[3] O. Laumann and C. Bormann. Elk: The extension language kit. ftp://ftp.cs.indiana.edu:/pub/scheme-repository/imp/elk-2.2.tar.gz, Technische Universit�t Berlin, Germany.

[4] J. Ousterhout, "Tcl: an embeddable command language", Proc. of the Winter 1990 USENIX Conference. USENIX Association, 1990.

[5] D. Cowan, R. Ierusalimschy, and T. Stepien, "Programming environments for end-users", 12th World Computer Congress. IFIP, Sep 1992, pp. 54–60 Vol. A-14.

[6] L. H. Figueiredo, C. S. Souza, M. Gattass, and L. C. Coelho, "Gera��o de interfaces para captura de dados sobre desenhos", V SIBGRAPI, 1992, pp. 169–175.

[7] R. Ierusalimschy, L. H. Figueiredo, and W. Celes, "Reference manual of the programming language Lua version 2.1", Monografias em Ci�ncia da Computa��o 08/95, PUC-Rio, Rio de Janeiro, Brazil, 1995. (available by ftp at ftp.inf.puc-rio.br/pub/docs/techreports).

[8] B. Beckman, "A scheme for little languages in interactive graphics", Software, Practice & Experience, 21, 187–207 (1991).

[9] J. Bentley, More programming pearls, Addison-Wesley, 1988.

[10] A. V. Aho, B. W. Kerninghan, and P. J. Weinberger, The AWK programming language, Addison-Wesley, 1988.

[11] J. K. Ousterhout, Tcl and the Tk Toolkit, Addison-Wesley, 1994.

[12] L. Wall and R. L. Schwartz, Programming perl, O'Reilly & Associates, Inc., 1991.

[13] L. Lamport, LaTeX: A Document Preparation System, Addison-Wesley, 1986.

[14] D. Ungar et al., "Self: The power of simplicity", Sigplan Notices, 22(12), 227–242 (1987) (OOPSLA'87).

[15] C. H. Levy, L. H. de Figueiredo, C. J. Lucena, and D. D. Cowan. "IUP/LED: a portable user interface development tool", Software: Practice & Experience 26 #7 (1996) 737–762.

[16] M. T. de Carvalho and L. F. Martha, "Uma arquitetura para configura��o de modeladores geom�tricos: aplica��o a mec�nica computacional", PANEL95 - XXI Confer�ncia Latino Americana de Inform�tica, 1995, pp. 123–134.

[17] C. Nahaboo. A catalog of embedded languages. ftp://koala.inria.fr:/pub/EmbeddedInterpretersCatalog.txt.

[18] B. W. Benson Jr., "libscheme: Scheme as a C Library", Proceedings of the 1994 USENIX Symposium on Very High Level Languages. USENIX, October 1994, pp. 7–19.

[19] A. Sah and J. Blow, "A new architecture for the implementation of scripting languages", Proc. USENIX Symposium on Very High Level Languages, 1994.

[20] A. Baird-Smith. "OScheme manual". http://www.inria.fr/koala/abaird/oscheme/manual.html, 1995.

[21] A. Sah, "TC: An efficient implementation of the Tcl language", Master's Thesis, University of California at Berkeley, Dept. of Computer Science, Berkeley, CA, 1994.

[22] Sun Microsystems, Java, The Language, 1995. http://java.sun.com/people/avh/talk.ps.

[23] G. van Rossum, "An introduction to Python for UNIX/C programmers", Proc. of the UUG najaarsconferentie. Dutch UNIX users group, 1993. (ftp://ftp.cwi.nl/pub/python/nluug-paper.ps).

[24] G. van Rossum. Python frequently asked questions, version 1.20++. ftp://ftp.cwi.nl/pub/python/python-FAQ, March 1995.

[25] S. Yemini and D. Berry, "A modular verifiable exception handling mechanism", ACM Transactions on Programming Languages and Systems, 7(2) (1985).

[26] A. Black, "Exception handling: the case against", Ph.D. Thesis, University of Oxford, 1982.

[27] R. Cerqueira, N. Rodriguez, and R. Ierusalimschy, "Uma experi�ncia em programa��o distribu�da dirigida por eventos", PANEL95 - XXI Confer�ncia Latino Americana de Inform�tica, 1995, pp. 225–236.