Skip to content
Hisai Toru edited this page Apr 14, 2013 · 21 revisions

Lua の特徴的な機能

関数と多値の受け渡し

関数呼び出し

だいたいのプログラミング言語と同様に、Lua でも関数呼び出しの機能があり、通常は関数名の後に括弧で囲んだ引数リストを伴って呼び出す。たとえば、次のような形である。

~~codetmp(function(spin)
local res = func(1, 2, 3)
~~end)

いくつかの短縮記法

引数が 1 個で、かつそれが文字列かテーブルのリテラルの場合に限り、引数リストを囲む括弧を省略できる。たとえば、引数が文字列の場合は次のようにかける。

~~codetmp(function(spin)
local res = func "HELLO!"
~~end)

これは、次のように書くのと同じ意味である。

~~codetmp(function(spin)
local res = func("HELLO!")
~~end)

テーブルの場合はこのような形である。

~~codetmp(function(spin)
local res = func {a = "one", b = "two"}
~~end)

これも、次の文と同じ意味になる。

~~codetmp(function(spin)
local res = func({a = "one", b = "two"})
~~end)

上のような短縮記法を使ったところで、特別コードが短くなる訳ではないが、たとえばソフトウェアの設定ファイルの記法として Lua を採用する場合などは、より簡潔にかけた方が良い事もある。たとえば、3 次元モデルのような階層構造を持ったデータを表したい場合、昔懐かしい VRML の表記を少しアレンジして次のように書ける。

~~codetmp(function(spin)
Shape {
  geometry = Cylinder { 
    height = 4,
    radius = .5
  }
}
~~end)

関数定義

このような関数を定義するには、次のように書く。

~~codetmp(function(spin)
function func (a, b, c)
  -- a、b、c を使って何かする
  return result
end
~~end)

これは、func という名前の関数を定義していると考えることができるが、じつは、次のような書き方と同じ意味になる。

~~codetmp(function(spin)
func = function (a, b, c)
  -- a、b、c を使って何かする
  return result
end
~~end)

何が違うかというと、function から end までの間に記述された 無名関数func という変数に代入しているのだ。もう少し細かくいうと、関数と変数が同じ名前空間に属するということと、JavaScript のように関数の定義のところで実行順が変わったりしないということができる。

また、func が単なる変数であるということは、次のようにローカル変数にすることもできる。

~~codetmp(function(spin)
local func = function (a, b, c)
  -- a、b、c を使って何かする
  return result
end
~~end)

これも次のように少し短く書くことができる。

~~codetmp(function(spin)
local function func (a, b, c)
  -- a、b、c を使って何かする
  return result
end
~~end)

つまり、頭に local をつければ、その関数はローカルのスコープを持つということである。

多値と引数

多くのプログラミング言語では、関数は一度に 1 個しか値を返すことができないが、Lua では任意の数の値を一度に返すことができる。この時、関数が 多値 を返すといったりする。たとえば、3 個の引数をとって、これらをそれぞれ 3 倍して返す関数は次のように書ける。

~~codetmp(function(spin)
function three_times (a, b, c)
   return a * 3, b * 3, c * 3
end
~~end)

このように、返したい値をカンマで区切って return すればよい。

こうして返された多値は、次のようにして受け取る。

~~codetmp(function(spin)
local x, y, z = three_times(1, 2, 3)
~~end)

代入文で、複数の変数をカンマで区切って指定するだけである。

多値をそのまま、関数の引数とすることもできる。たとえば、3 個の引数をとってそれらの合計を計算する次のような関数があるとする。

~~code("sum.lua", function(spin)
function sum (a, b, c)
   return a + b + c
end
~~end)

この関数に対して、

~~code("multi-value-compose.lua", function(spin)
local x = sum(three_times(1, 2, 3))
~~end)

というふうに呼び出すと、three_times からの 3 個の戻り値がそのまま sum の 3 個の引数として渡される。

このように、関数が返す多値と関数へ渡す引数の間には対照的な関係がある。これは Lua の面白い特徴のひとつである。

多値は複数の値をひとまとめにしたものなので、一見したところ配列(テーブル)と同じように思えるかもしれないが、多値をデータ構造として扱うことはできない。ただし、多値あるいは引数の列を配列に変換したり、配列を多値に変換することはできる。

引数の列を配列にするには、次のように ... という記号を使う。

~~code("maltivalue-array.lua", function(spin)
function three_times2 (...)
   local vals = {...}

   for i, v in vals do
      vals[i] = 3 * v
   end
   
   -- 計算結果を返す……
end
~~end)

逆に配列を多値に展開するには table.unpack という関数を使う。これを使って上の関数を完成させると、次のようになる。

~~code("maltivalue-array-2.lua", function(spin)
function three_times2 (...)
   local vals = {...}

   for i, v in vals do
      vals[i] = 3 * v
   end
   
   return table.unpack(vals)
end
~~end)

また、配列を引数リストとして関数を呼び出すにも table.unpack を使う。これは JavaScript や Scheme などの言語における apply と同様のことをする。

~~code("array-maltivalue.lua", function(spin)
local numbers = {1, 2, 3, 4}
local a, b, c, d = three_times2(table.unpack(numbers))
~~end)

このように配列と多値や引数リストの間での変換が自在にできるようになると、非常に柔軟なプログラミングができるようになる。

クロージャ

無名関数とレキシカルスコープ

クロージャとは、関数とそれを取り巻く「環境」を、無名関数の形で閉じ込めたものである。 それは関数のように呼び出すことができ、評価すると値を返す。 しかし、普通の関数と違って、クロージャはそれぞれ固有の状態を持つことが出来る。

状態を持ち、関数として呼び出すことが出来るという点で、C++ の関数オブジェクトに近い。 ただし、関数オブジェクトと違って、いちいち名前をつける必要がないし、クラスを宣言する必要もない。 また、関数呼び出しというシンプルなインターフェイスしか持たないため、 どんなところでも使うことが出来る。

例を示すために、まずは通常の関数を考える。たとえば与えられた数値を 4 倍した数値を返す関数 four_times は次のように書ける。

~~code("4times.lua", function (spin)
function four_times(m)
   return m * 4
end
~~end)

ここで、4 倍するのに飽きて 5 倍にする関数が欲しくなったとしよう。このとき、four_times の関数定義をコピーして five_times 関数を作るのは簡単だが、飽きるたびに新しい関数を作っていてはきりがない。そこで、好きな数値 n を与えると、「n 倍する関数」を作ってくれる関数を作ると便利だろう。

これは、Lua では次のように簡単に作ることができる。

~~code("closure.lua", function (spin)
function n_times(n)
   return function(m)
             return m * n
          end
end
~~end)

ここで、function が 2 重に入れ子になっていて、内側の function で作った関数が、外側にある n_times という関数の返り値となっていることが分かる。また、内側の関数には名前がついてなくて、function のすぐ次に引数リストが来ていることにも注意してほしい。このような関数は名前がついてないので無名関数と呼ばれる。

無名関数は、変数に代入したり、関数の引数にしたり、あるいは関数からの返り値にすることができる。このような性質から、無名関数は数値や文字列などと同様の第一級オブジェクト(first-class object)といったりする。

さてこの内側の関数をよく見ると、外側の関数から持ち込んだ変数 n を参照しているのが分かる。この n はグローバル変数ではない。なぜなら、n_times 関数の内側でしか使えないからだ。このように、関数の外側で宣言され、かつグローバルでもない変数を参照することができる。このような変数のスコープのことをレキシカルスコープという。

クロージャ

上で紹介したような無名関数とレキシカルスコープを組み合わせて作った関数のことをクロージャと呼ぶ。クロージャは日本語では閉包と言われたりもする。これは関数に状態を閉じ込めて包んだものである。この状態とは何か、以下で考えてみる。

上の n_times は、一度関数を作ってしまうと、その関数はいつ、何度呼んでも、引数が同じならば同じ結果を返した。では次に、呼び出すたびに値が変化するような関数を考えよう。たとえば、呼び出すたびに値が 1 づつ大きくなる関数を作ってみる。

~~codetmp(function(spin)
count = 0
function count_up()
   count = count + 1
   return count
end
~~end)

この関数は確かに呼び出すたびに 1 づつ大きな整数を返す。しかし、グローバル変数を使っているためバグを生みやすい。またグローバル変数を使っているために、同じ動作をする関数をもうひとつ作るには、もうひとつグローバル変数を用意しなくてはいけない。クロージャを使うと次のようにかける。

~~codetmp(function(spin)
function make_counter()
   local count = 0
   return function()
      count = count + 1
      return count
   end
end
~~end)

エラー処理と大域脱出

pcall

関数の実行中にエラーが発生した場合、通常は呼び出し元までエラーが伝搬し、そこで実行を止めてしまう。しかし、アプリケーション組み込みのスクリプトでは特に、エラーが発生しても止まってほしくないことがある。そこで、単に関数を呼び出す代わりに pcall() という関数を使って関数を呼び出す。

pcall (f [, arg1, ···])

pcall()保護モード(protected mode)という特殊なモードで関数を呼び出す。 http://www.lua.org/manual/5.2/manual.html#2.3

関数を呼び出す関数というと少しややこしい。言い換えれば、Lua はエラーハンドリングさえも関数呼び出しの枠組みで処理し、他の言語における例外のような言語要素を持たない。これはこれで言語仕様がシンプルであるともいえるが、Lua という言語において、シンプルであることが必ずしも理解が簡単であることと同じではないことの例でもある。

さてここで、f は呼び出したい関数で、arg1, ... はそれに渡したい引数である。pcall() は関数の呼び出しが成功すれば true と関数からの返り値を返す。失敗すると false とエラーメッセージの文字列を返す。従って、関数 f の返り値の数が N だとしたら、pcall() の返り値は N+1 個ということになる。

~~code("error-handle.lua", function (spin)
function may_have_error(a)
   if a then
      error("something is wrong")
   end
   return "no error"
end
local result, mesg = pcall(may_have_error, true)
if not result then
   print("[ERROR]", mesg)
end
~~end)

このコードを実行すると、次のようなメッセージを表示する。

[ERROR] eg/error_handle.lua:3: something is wrong

これを見ると分かるように、エラーメッセージにはエラーが発生したファイルの行が分かる。

なお、error() という関数は意図的にエラーを発生させるための関数である。

error (message [, level])

ここに message はエラーメッセージ文字列であり、level を指定することで「どこでエラーが発生したことにするか」を決めることができる。これは後に述べるアプリケーション組み込みのスクリプティング環境を作るのに役に立つ。

error による大域脱出

エラーを報告するという目的の他に、もうひとつ error() の使い途がある。それは、何重もの関数呼び出しの深層から一気に呼び出し元に制御を戻す大域脱出である。

~~code("global-exit.lua", function (spin)
function f()
   g()
end
function g()
   h()
end
function h()
   error("exit")
end
f()
~~end)

コルーチン

Lua の言語要素の中で特に特徴的なのがコルーチン(coroutine)である。コルーチンとは、関数のように呼び出すことができて、また値を返すものである。ただし関数と違うのは、一度値を返したコルーチンを再び呼び出すと、そのコルーチンはさっきの続きから処理を再開するところである。

コルーチンは Scheme の継続にも似ている。 この点で、Lua のコルーチンはワンショット継続(one-shot continuation)と呼ばれることもある。

関数とその呼び出し元の逆転

一般的な意味での関数呼び出しは、通常は呼び出す側と呼び出される側に絶対的な違いがある。呼び出された関数は、呼び出し元に対して値を返す(return)ことで情報を渡す。

~~code("ordinary-func.lua", function (spin)
function main()
   local value = subroutine("arg")
end

function subroutine(param)
   return "hello " .. param
end
~~end)

上のコードでは、main()subroutine() を呼び出している。

しかしコルーチンを使うと、あたかも何か別の関数を呼び出すようにして、呼び出し元に値を返すことができる。

~~code("inside-out.lua", function (spin)
function main()
   local subco = coroutine.wrap(subroutine)
   local value = subco("arg")
end

function retvalue(value)
   coroutine.yield(value)
end

function subroutine(param)
   retvalue("hello " .. param)
end
~~end)

このコードも同様に、main()subroutine() を呼び出している点は変わらない。しかし、subroutine() は値を return する代わりに、retvalue() という関数を呼び出している。つまり、main() からみた時と、subroutine() からみた時で、関数呼び出しの主従関係が逆転しているのだ。

これはゲームやユーザインターフェイスなど、人間と対話的に動作するプログラムを記述するときに便利である。

メタテーブルと環境

メタテーブル

Lua のテーブルは Lua がもつ唯一のデータ構造であるが、メタテーブルを使うことで、テーブルに対するアクセスのやり方を必要に応じて変えることができる。メタテーブルそのものもまたテーブルとして表現される。

環境

Lua の関数は、呼び出されるときに必ず 1 個の環境を伴って呼び出される。環境とは、関数の外側で宣言された変数とその値の対応関係である。つまり、関数の中から見るとグローバル変数のように見える。この環境の作り方は Lua 5.1 と Lua 5.2 で少し違うので、以下では Lua 5.2 での方法を説明する。

環境をセットするには、単に _ENV という名前のテーブルを書き換えるだけでよい。あるいは、新しいテーブルをつくって _ENV という変数に代入しても良い。

たとえば、次のような関数があったとする。

~~code("env-normal.lua", function (spin)
function g()
   print "global g"
end

function f()
   g()
end
~~end)

この中ではグローバルな関数 g() を関数 f() の内部から呼び出している。この f() をそのまま呼び出すと

global g

という出力が得られる。ここで、次のように _ENV を新しく設定して呼び出してみる。

~~code("env-overridden.lua", function (spin)
_ENV = {print = print,
        f = f,
        g = function()
               print "local g!"
            end}
f()
~~end)

すると今度は、

local g!

という出力が得られる。関数 f() の中から参照している変数 g の定義が、_ENV の要素として与えられているからだ。また、_ENV では g だけでなく、printf の値も設定されている事に注意すること。これらがないと変数の値が定義されていないので呼び出しに失敗してしまう。

組み込みスクリプティング環境の構築

協調的マルチスレッド

ゲームのように、多数のキャラクターがそれぞれ独立して動くような動作をさせたい場合は、Lua のコルーチンを使った協調的マルチスレッド(collaborative multithreading)を使うと便利な場合がある。ゲームを進行させる最小の時間単位をフレーム(frame)と呼ぶ。ここで、ゲームの世界にいるすべてのキャラクターに対して、update() というメソッドを毎フレーム呼ぶとする。

~~codetmp(function(spin)
function game_character:update()
   -- キャラクターの動作を 1 フレーム分進める
end
~~end)

さてここで、キーを押すとキャラクターが歩き、何かの条件が揃うとゲームオーバーになるような簡単なゲームを考えよう。従来のゲーム開発では、キャラクターの状態遷移に着目して、update を定義する事が多かった。

~~codetmp(function(spin)
function game_character:update()
   -- キャラクターの動作を 1 フレーム分進める
   if self.state == "init" then
      -- 初期状態の処理
   elseif self.state == "idle" then
      -- アイドル状態の処理
   elseif self.state == "walking" then
      -- 歩いている状態の処理
   elseif self.state == "gameover" then
      -- 終了処理
      -- ... などなど
   end
end
~~end)

だいたい上のコードのように、現在の状態によって動作の内容を決定し、決められた条件が成立したり、何かのイベントが発生したときに状態を変更する。これはこれで、キャラクターの状態遷移が明確に文書化されていれば分かりやすいが、コードの中をあっちこっちにジャンプするため、少し見通しが悪い。

ここで、コルーチンを使うと、大まかな処理の移り変わりは次のようにかける。

~~codetmp(function(spin)
function game_character:update_coro()
   -- 初めに一度だけ初期化する
   self:initialize()
   
   -- 終了条件が成立するまで無限ループ
   while true do
      while not self:key_pressed() do
         -- キー入力があるまでアイドル状態の処理を繰り返す
      end
      while not self:key_released() do
         -- キーが話されるまで歩いている状態の処理を繰り返す
      end
      if self:is_gameover() then
         -- 終了条件が成立したらループを抜ける
         break
      end
      coroutine.yield()     -- 次のフレームまで待つ
   end
   
   -- 終了処理
end
~~end)

ここで、この関数がコルーチンとして呼び出されるつもりである事を表すために、末尾に _coro を付けた。さて、このコルーチンを実際に update 関数から呼び出すには、まずコルーチンのオブジェクトを作って、これを実行する。

~~codetmp(function(spin)
function game_character:update()
   -- 初回にコルーチン(スレッド)を作成する
   if not self.coro then
      self.coro = coroutine.create(self.update_coro)
   end
   
   -- コルーチンが生きていたら再開する
   if coroutine.status(self.coro) ~= "dead" then
      coroutine.resume(self.coro, self)
   end
end
~~end)

update() メソッドは何度も何度も繰り返し呼ばれるが、コルーチンは最初の一度だけ作ればよい。コルーチンを作るには coroutine.create() という関数を呼ぶ。

coroutine.create (f)

ここで f は関数である。coroutine.create() は関数を受け取って、それを実行するコルーチンを作成して返す。これで得たコルーチンを self.coro というメンバ変数にセットしている。update() メソッドは実行中に何度も何度も繰り返し呼ばれるが、self.coro は一度だけセットされる事に注意すること。

つぎに、coroutine.status() という関数を使ってコルーチンが有効かどうかをテストする。

coroutine.status (co)

この関数は、コルーチンの状態に応じて、それぞれ "running"(実行中)、"suspended"(一時停止中)、"normal"(まだ実行されていない)、"dead"(終了)という文字列を返す。ここでは "normal""suspended" であればコルーチンを実行したいが、"running" という状態はあり得ないので、「~= "dead"」という比較で間に合わせている。

コルーチンを実行または再開するには coroutine.resume() を用いる。

coroutine.resume (co [, val1, ···])

ここで co は実行したいコルーチン、第 2 引数以降にオプショナルで、そのコルーチンに渡す引数を与える事ができる。ここでは self を渡している。これは game_character:update_coro() では暗黙の第 1 引数として受け取っているものである。

さてここで、もう一度 game_character:update_coro() をみてみると、ゲームの各フェーズ(あるいは状態)の間の遷移を、whileif のような Lua がもともと持っている「普通の」制御構造を使って記述している事が分かる。先に紹介した状態遷移方式では、大域的な制御構造を素直に記述できない。普通の制御構造がそのまま使える事で、キャラクターのロジックがきちんと停止するか、イベントに対してしかるべき反応をするか、などをコード上で簡単に確かめる事ができる。

見かけ上のメインループ

上の game_character:update_coro() は、全体が while true do で始まる無限ループとして書かれている。この無限ループは、ゲームやユーザインターフェイスのプログラミングでよく用いられるメインループ、またはイベントループと呼ばれるループと同じ形をしている。

普通は、このような無限ループは、プログラムの中で 1 個だけ存在するものであり、ゲームに出てくるキャラクターのひとつひとつが持つようなものではない。しかし、コルーチンを使う事で見かけ上のメインループをそれぞれのキャラクターに持たせる事ができるので、それぞれの動作を素直に記述する事ができる。

これによる利点は、既に述べたように普通の制御構造を使えるという事の他に、一時的な記憶のためにグローバル変数やインスタンス変数を持たなくて良いということもあげられる。状態を小さな関数に閉じ込めておけるために、余計なバグのもとを減らす事ができるのである。