Lua で組んでみる


汎用組み込み用スクリプト言語 Lua でのプログラムの書き方の説明です。

Lua / 特徴 / 実行 / 言語仕様 / 発展 / オブジェクト指向 / 5.1→5.2 / 戻る / トップページ


Lua とは

Lua は組み込み用の汎用スクリプト言語です。 2011/12/16 にバージョン 5.2 (2006/2/21 にバージョン 5.1) が リリースされました。 ちなみに、Lua の名前はポルトガル語で「月」の意味です。
 
シンプル・コンパクト・高速といった無駄のなさ、 CPU を含むリソース消費量の少なさが特徴です。 そのため、ゲームへの組み込み用途に向いています。 実際に海外のゲームでいくつか採用実績があり、それなりにメジャーです。
 
Lua は使われているわりには、日本語でのドキュメンテーション関係が 不足しています (国内のサイトでは公式サイトの日本語訳である Lua言語の紹介 (Lua 4.0), Lua 5.2 リファレンスマニュアル が有名です)。 そこで、自分の覚え書きも兼ねてここに簡単にまとめておこうと思います。 Lua の実行系を自分のプログラムに組み込む方法を解説しているサイトは いくつかあるようですので、ここでは Lua でのコーディングに絞って 書きます (プログラム言語を知っている人向けに書いています)。

特徴

Lua は言語そのものに多くの機能を実現するのではなく、様々な機能を 実現するためにわずかな仕掛けを作っておくことをコンセプトとしています。 つまり、シンプルで拡張性が高い特徴を持っています。
 
Lua は手続き記述型の言語で、Pascal 言語っぽい書き方になります。 基本的にはオブジェクト指向ではありません。 インタープリタ・コンパイラともあります。 使いやすい機能としては、連想配列、動的型付け、自動メモリ割り当てと ガベージコレクション機能などがあります。

実行方法

Lua は組み込み型言語ですので、本来は他のアプリケーションに実行系を 組み込んで実行させますが、テスト用に単体でも実行できます。 ただし、Lua 自身はコンパクトに収める設計思想で出来ているので、 この実行方法ではごく基本的な 標準関数 しかありません (とはいえ、標準入出力やファイルの読み書き、 正規表現のパターンマッチングなどは用意されてます )。
 
このテスト実行は次のようにすれば行えます。
 
公式サイトの Lua Binaries Download から 自分のプラットフォームにあった実行環境をダウンロードします。 Windows 環境であれば、ファイル名は lua5_1_Win32_bin.tar.gz です。 tar.gz 圧縮されていますので、展開しましょう。
 
適当なテキストファイルに Lua のプログラムを書きます。 お約束の "Hello World" ならこの一行のテキストファイルで十分です。 とてもシンプルです。
	print "hello world!"
展開先のフォルダにあるインタープリタ (Windows 環境で、Lua のバージョンが 5.1 であれば、lua5.1.exe) の引数に先ほど書いたテキストファイルを 指定すれば実行できます。
	>lua5.1 test.lua
	hello world!
他の手段として、公式サイトの Demo で、 フォームに入力したプログラムをその場で実行することができるので、 ちょっと試したい場合はこれを使うとよいでしょう。

言語仕様 (基本編)

以下は、Lua に比較的特徴的な言語機能について順番に列挙します。 詳細な仕様は マニュアル (日本語訳) を参照してください。
 
処理の流れ
 
組み込み専用言語なので、main() 関数はありません。 ファイルの頭から実行されるか、特定の関数が実行されるかは呼び出し元 次第でいかようにもなります。
 
上記のテスト実行の際はファイルの頭から処理されます。
 
コメント
 
コメントは -- から始めて、行末までです。 複数行をまたぐコメントは --[[ から ]] までです。
	-- コメント文
	
	--[[
		コメント文
	]]
 
型は nil、実数、ブーリアン、文字列、関数、連想配列などがあります。 宣言は不要です。
	a=1
	b="test"	-- 文字列はダブルクォートでくくる
	c=nil		-- nil は未定義値、偽を表す特殊な値
	d=true
代入や引数渡し・関数の戻り値は、常に値への参照で扱われ、 値のコピーは行われません。
 
連想配列 (テーブル)
 
連想配列とは、添え字に任意の値が指定できる配列です。 Lua には普通の配列は存在せず、全て連想配列扱いとなります。
	list={}
	list[1]="eins"
	list["two"]="zwei"
	list[3]="drei"
なお、連想配列への代入はいきなり a[1] = 3 とはできず、 一旦 a = {} のように変数に空の連想配列を代入する必要があります。
 
リスト表記することで連想配列の値を一気に代入することもできます。 例えば、a = {"v1", "v2", 34} とすると、 a[1] = "v1"; a[2] = "v2"; a[3] = 34 となります。 添え字が 1 から始まっている点に注意しましょう。 また、b = {x = 1, y = 3} と書くと、b["x"] = 1; b["y"] = 3 となります。
 
添え字に自然数しか取らないことで、配列のようにも扱えます。 その時 (値に nil を取らないのであれば)、# 演算子で要素数を取れます。
	a={12,15,20}
	print(#a)
	-- 実行結果は 3
ブロック
 
文末にはセミコロンを入れても良いです。 文 (正確にはチャンクと呼ぶ) は do〜end でくくってブロックにできます (C 言語の { } に相当)。
	x=100
	
	do
		y=5
		z=30
	end
関数定義
 
関数の定義も命令であるため、そこに処理が流れた時に関数が 使えるようになります。 組み込み先の言語 (C++ など) から関数名を指定して 呼んでもらうこともできます。
	function multiply(x,y)
		return x*y
	end
変数のスコープ
 
変数は基本はグローバル、 "local hoge" などとすると hoge はローカル変数になります。 レキシカルスコープ (lexical scoping) です (C 言語の auto 変数、Perl 言語の my 宣言と同じ)。
 
ローカル変数の初期値は nil ですが、 ローカル変数はその宣言の直後から有効なため、 "local hoge=hoge" とすることでグローバル変数の値を代入できます。
	function print_value(x)
		local x=x
		print(x)
		x=5
		print(x)
	end
	
	x=3
	print(x)
	print_value(x)
	print(x)
	-- 実行結果は 3, 3, 5, 3
local 文を実行するたびに新しいローカル変数が定義される点に注意です。
	for i=1,10 do
		local y
		print(y)
		if y==nil then y=0 else y=y+1 end
	end
	-- 実行結果は nil が 10 個表示される (10 回ローカル変数が定義される)
if 文
 
if 文は "if 評価式 then 〜 else 〜 end" の形、 else if の時は "elseif" と書きます。
 
評価式は nil, false が偽、それ以外が真。 型が違う値どうしの比較は全て偽なので、 "0"==0, false==nil, ""==nil はどれも偽になる点に注意です。
	a=123
	b="123"
	
	if a==b then
		print("equal")
	else
		print("not equal")
	end
	-- 実行結果は not equal
while 文
 
while 文は "while 評価式 do 〜 end"、 repeat 文は "repeat 〜 until 評価式" と書きます。
	i=5
	while i>0 do
		print(i)
		i=i-1
	end
	repeat
		print(i)
		i=i+1
	until i>5
	-- 実行結果は 5, 4, 3, 2, 1, 0, 1, 2, 3, 4, 5
ループから抜け出すには "break" 文が使えます。 デバッグ時以外でそのような状況はめったにありませんが、 break 文はブロックの最後にある必要があるため、 ブロックの途中で使用する場合は do〜end でくくる必要があります。
	p=7
	while true do
		p=p+1
		-- break だけではエラーになる。
		do break end
		p=p-1
	end
for 文
 
for 文は "for 変数 = 初期値, 終了値, 増加量 do 〜 end" と書きます。
	total=0
	for i=0,100,1 do
		total=total+i
	end
	print(total)
	-- 実行結果は 5050
連想配列を総なめするなら、 "for 添え字を入れる変数, 値を入れる変数 in pairs(連想配列) do 〜 end" と書きます。
	list={}
	list[1]="eins"
	list["two"]="zwei"
	list[3]="drei"
	
	for key,value in pairs(list) do
		print(tostring(key).."="..tostring(value))
	end
	-- 実行結果は 1=eins, 3=drei, two=zwei
goto 文
 
バージョン 5.2 から使える新機能として、goto 文が使えます。
	a=0
	while true do
		a=a+1
		
		if a>3 then
			goto exit
		end
		
		print(a)
	end
	::exit::
	-- 実行結果は 1, 2, 3
ラベル名を :: で前後をくくって書き、goto ラベル名で飛びます。
 
ローカル変数のスコープ内に入らない限りは、ラベルの見える範囲なら どこからどこへも飛べますが、乱用すると当然メンテナンス性を損なうため、 多重ブロックからの脱出など必要性の高い場合に限って使いましょう。
 
多重代入
 
多重代入ができます。右辺の値と左辺の添字がすべて評価された後に 代入が行われるため、"x, y = y, x" で値を入れ替えれます。なかなか便利です。
 
また、関数の返り値も複数返せます。 "return a, b" といった感じで書きます。
	function swap(x,y)
		x,y=y,x
		return x,y
	end
	
	a=1
	b=2
	
	print(a,b)
	a,b=swap(a,b)
	print(a,b)
	-- 実行結果は 1, 2, 2, 1
可変数の引数
 
関数の仮引数のリストの最後に ... を置けば、可変の引数リストを持つ 関数が作れます。 ... の部分に引数として渡された値は ... と表記する特殊な名前の 変数で参照します。この特殊な変数は代入文の右辺や、 組み込み関数 select の引数にのみ指定可能です。 関数 select は、select("#", ...) で引数の数を、 select(インデックス, ...) でインデックス番目以降の値を取り出す関数です。
	function f(a,b,...)
		print(a,b)
		c,d=...
		print(c,d)
		for i=3,select("#",...),1 do
			x=select(i,...)
			print(x)
		end
	end
	
	f(11,12,13,14,15,16,17)
	-- 実行結果は 11, 12,  13, 14,  15, 16, 17
日本語対応
 
Lua ライブラリ自体を改良しない限り、文字コードへの対応はやや不十分です。
 
8 ビット目は問題なく処理するように作られており、 ソースコードが S-JIS や Unicode で書いてあってもそのままのバイナリ列と して組み込み先の言語に渡されるようになっています。 そのため、組み込み先にあわせた文字コードでソースを書くことが可能です。
 
これは一見便利なのですが、S-JIS の「ソ」「ー」等の文字の 2 バイト目 (「\」と同じ文字コードになる) などは考慮されないまま 処理されるため、日本語文字がうまく扱えない場合があります。
 
このような問題が起きないように UTF-8 で書いたり (こうすると、 上記のテスト実行の場合は print 文なども UTF-8 で出力されて しまって読めませんが)、Lua を改良すると良いと思います。

言語仕様 (発展編)

以下の仕様は知らなくても Lua のプログラムはできますが、 知っているとより機能的な書き方ができます。
 
連想配列の別表記
 
ドット区切りで連想配列の値を表現することも出来ます。
 
var.NAME の形式は var["NAME"] と等価です。
 
関数定義の別表記
 
Lua では全ての関数は値になります (関数もファーストクラスオブジェクト)。 つまり関数定義も、関数名の名前を持つ変数に、 関数の実体を代入していることになります。
	multiply=function(x,y)		-- function multiply(x,y) と同義
		return x*y
	end
	
	print(multiply(2,3))
	
	-- 実行結果は 6
関数の定義はこのように代入の形で定義できます。 関数も値であることを考えれば、 こちらの表記の方が厳密な書き方なのかもしれません。
 
関数呼び出しの別表記
 
t:f(x) という表現は t.f(t,x) と等価になります。 ちょっとオブジェクト指向っぽいですが、 そのような拡張を行うための仕掛けの一つです。
 
"function v:f (...) 〜 end" は "v.f = function (self, ...) 〜 end" と 等価です。
 
可変引数と連想配列の相互変換
 
可変引数と連想配列は別の概念ですが、 可変引数を中括弧でくくることで連想配列に、 連想配列を unpack 組み込み関数に渡すことで可変引数に、相互変換できます。
	function f(...)
		print(...)
		
		a={...}				-- 可変引数→連想配列
		print(a[1],a[2],a[3])
		print(unpack(a))		-- 連想配列→可変引数
	end
	
	f(1,2,3)
	-- 実行結果は 3 行とも 1, 2, 3
擬似的な条件演算子
 
Lua には条件演算子 (C 言語の 〜 ? 〜 : 〜 ) はありませんが、 and, or 演算子を使って擬似的に作ることができます。 "条件式 and 真の場合の値 or 偽の場合の値" とします。
	a=3
	b=1
	
	c=(a>b) and a or b
	
	print(c)
	
	-- 実行結果は 3
ただし、あくまで擬似的なものであるため、真の場合の値に false と nil が使えない (偽の場合の値が採用されてしまう) ので、 あまり使わない方が良いでしょう。
 
モジュール (バージョン 5.2 では非推奨)
 
モジュールの定義は、定義する側のファイルの頭でモジュール名を引数に module 関数を実行します。以後、module 関数の引数で指定した名前の テーブルに環境が変更されます。
 
その副作用として、グローバル環境で定義されている関数の呼び出し (組み込み関数の math.random() 等も含む) もできなくなってしまいます。 そのため、module 関数の引数の 2 番目に package.seeall を指定することで この対処が可能になっています。
	-- ファイル mod_test.lua
	
	module("mod_test",package.seeall)
	
	value=2
	
	function box_muller()
		local alpha=math.random()	-- package.seeall のおかげで使える
		local beta =math.random()
		
		return math.sqrt(-2*math.log(alpha))*math.sin(2*math.pi*beta)
	end
モジュールを呼び出す側は require 関数を使います。
	require "mod_test"
	
	print(mod_test.value)
	print(mod_test.box_muller())
	-- mod_test.lua で box_muller の名前で定義した関数が呼ばれる
	
	-- 実行結果は 2, -1.422 など
モジュールファイルは C 言語など、他の言語で実装されたものも呼び出し可能で、 呼ぶ側はそれが何で実装されたものなのか意識する必要はありません。
 
完全なクロージャ
 
クロージャ (closure) とはソースコード上のまとまりの単体です。
 
関数内に出現する、関数内で宣言されていない変数の解決の際、 実行時の環境ではなく関数を定義した環境 (クロージャ) の変数を参照します。 これを完全なクロージャと呼び、Lua では完全なクロージャが実装されています。
	a = {}
	local x = 20
	for i=1,10 do
		local y = 0
		a[i] = function () y=y+1; return x+y end
	end
	
	for i=1,10 do
		print(a[i]())
	end
	-- 実行結果は 21 が 10 個表示される
10 個の a[i] はそれぞれ別の匿名関数で、それぞれにクロージャを作ります。 クロージャはすべて同じ x を参照していますが、 それぞれ異なった y を参照しています (y の解決を実行時の環境ではなく、 定義時の環境で行っているため)。
 
メタテーブル
 
ある値に対して演算 (四則演算や比較演算、連想配列の参照など) を 行おうとした時、計算可能な型ではなかったり値が偽となる時の挙動を、 通常の演算処理 (エラーや偽の値を返す) を行うのではなく、 指定した関数を呼び出すように指定することが出来ます。
 
このような、演算に対してどの関数を呼び出すのかを、 連想配列で表したものをメタテーブルと呼びます。
 
関数 setmetatable の引数にメタテーブルを設定したい値と、 メタテーブルを渡すことで、 メタテーブルの設定ができます。 メタテーブルには、演算を表す識別子を添え字に、 呼び出して欲しい関数を値に持たせます。
	obj={1,2,3,4,5,6}
	
	function plus(x,y)
		return x[1]+y
	end
	
	setmetatable(obj,{__add=plus})
	
	print(obj+5)
	-- 実行結果は 6
例えば、演算 __add では、+ 演算子のどちらかの項に 数値以外が指定された場合に、関数が呼ばれます。

言語仕様 (オブジェクト指向編)

Lua はオブジェクト指向言語ではありませんが、 発展編で説明した手法を組み合わせることで、 それらしい扱いをすることができます。
 
なお、ここで紹介するやり方は一つの方法に過ぎず、 他にも色々な実現方法があるようです。
 
オブジェクトの直接定義
 
連想配列とその表記のシンタックスシュガー、 関数がファーストクラスオブジェクトであることを利用して、 オブジェクトを定義できます (正確にはそれらしく見せることができる)。
	HogeObject = {
		value = 3,
		toString = function(self)
			return "HogeObject: value="..tostring(self.value)
		end
	}
	
	print(HogeObject:toString())
	
	-- 実行結果は "HogeObject: value=3"
オブジェクト HogeObject を生成し、メソッド toString() を呼んでいます。
 
このオブジェクトは実際は、連想配列 HogeObject に、 添え字 value に対して値 3、 添え字 toString に対して 中身を表す文字列を返す関数の実装、を入れたものです。
 
クラスの定義
 
先ほどの方法は、オブジェクトを直接生成しています。 このままではオブジェクトの数だけ定義文を書かなければなりませんので、 生成関数を作る形 (クラス定義のような定義方法) に書き換えます。
	HogeClass={}
	
	function HogeClass.new(value)
		local hogeObject = {
			value = value,
			toString = function(self)
				return "HogeClass: value="..tostring(value)
			end
		}
		
		return hogeObject
	end
	
	obj=HogeClass.new(5)
	print(obj:toString())
	obj2=HogeClass.new(7)
	print(obj2:toString())
	
	-- 実行結果は "HogeClass: value=5", "HogeClass: value=7"
関数 HogeClass.new (ここは別に newHogeClass のような連想配列を 使わない関数名であっても構いません) が先ほどのオブジェクトの 直接定義の方法を使ってオブジェクトを生成し、その値を返しています。
 
local 宣言のために、関数が呼ばれるたびに変数 hogeObject が 個別に生成されており、完全なクロージャのおかげで その中で定義される関数 toString はその環境を参照してくれます。
 
継承
 
継承の実現にはメタテーブルの __index 指定が使えます。
	HogeHogeClass={}
	
	function HogeHogeClass.new(value)
		local hogehogeObject = {
			toPlusOneString = function(self)
				return "HogeHogeClass: value="..tostring(value+1)
			end
		}
		local extends=HogeClass.new(value)
		
		setmetatable(hogehogeObject,{__index=extends})
		
		return hogehogeObject
	end
	
	obj3=HogeHogeClass.new(9)
	print(obj3.value)
	print(obj3:toPlusOneString())
	print(obj3:toString())
	
	-- 実行結果は "HogeHogeClass: value=10", "HogeClass: value=9"
メタテーブルの __index 指定に関数ではなく連想配列を指定した場合、 メタテーブルが設定された値に対して連想配列としての参照 (今回の使い方では メソッドの呼び出しに相当) を行い値が見つからなかったら、 代わりに指定した連想配列を参照するようになります。
 
この例でいえば、hogehogeObject にメタテーブルで、 オブジェクト (=連想配列) extends を指定しています。 オブジェクト obj3 (=hogehogeObject) のメソッド toString が呼び出されると、 値 toString を関数として実行するために まず連想配列である obj3 の添え字 toString の値を探します。 この値が存在しないので、メタテーブルの指定に従って 連想配列 extends (親クラスに属する実体) から値を探し、 HogeClass クラス側で定義した関数 toString を見つけてきます。 そして、それを関数として実行しています。

5.1→5.2 の変更点

Lua のバージョンアップは、(バージョン番号の小数部の変更であっても) 完全な上位互換にはなっていません。 とはいえ、細かい変更が多く劇的な変化があるわけではありません。 詳細な変更点は マニュアル (日本語訳) を参照してください。
 
バージョン 5.1 から 5.2 に上がった際の言語機能の変更点は、 高度な機能の挙動に関する点が多いため、普通に言語を使っている限りは あまり変化を感じないと思います。 一番大きいのは「環境」が Lua の関数のみになったことです。
 
言語仕様の変更に対応して、標準ライブラリの一部が変更されてますが、 それ以外にも変更があります。主な変更点は以下のとおりです。
 
 
もちろん新機能もあります。 ビット演算をする標準ライブラリ bit32 の追加、goto 分の追加、などがあります。

戻る