読者です 読者をやめる 読者になる 読者になる

Luaとあそぼう

くろねこさんがLuaを覚えたがっていたので。


ちなみに検証に使用したLuaのバージョンは現時点での最新版である5.2.2です。

LuaHello World

まずはお決まりのHello Worldから。書き方は3種類くらいあります。

-- はじめてのLuaプログラム
print("Hello World")
-- シングルクォートで囲う例
print('Hello World')
-- [[ 〜 ]] はヒアドキュメントのための構文
print([[Hello World]])

f:id:tercel_s:20130421222947p:plain
上記の3つのコードはいずれも文字列リテラルを表現する構文ですが、挙動が多少異なります。たとえば、

print("こんにちは\n今日はよい天気です")

というコードは、以下のように書いた場合と同様の実行結果になります。

print([[こんにちは
今日はよい天気です]])

Luaの変数

Luaは動的型付け言語なので、特に型指定なしに変数を使用することができます。

一般的な静的型付け言語では整数と浮動小数点数は区別されますが、Luaならば以下のようなスクリプトもエラーなく実行できます。

x = 10
y = 3.14
print(x + y)

なお、実行結果は「13.14」がコンソール上に表示されます。
f:id:tercel_s:20130421104646p:plain
文字列の場合、リテラルを連結するための特殊な構文が存在します。

str1 = "こんにちは"
str2 = "たーせるです"
print(str1..str2)		-- 「..」で文字列を連結します

実行結果はこんな感じです。
f:id:tercel_s:20130421104930p:plain

なお、C++ の null に相当するものは、Lua では nil 型として扱われます。たとえば未初期化の変数は nil になりますし、使用済みの変数を以後使用不可にするため、明示的に nil を代入することもあります。

グローバル変数とローカル変数

Lua の変数は、何もしないと自動的にグローバル変数になります。ただし、変数を作る際、変数名の前に local を付けるとローカル変数として扱われるようになります。

以下に例を示します。doendブロック(C++の波括弧に相当)の中で作られたローカル変数は、ブロックの外に出るときに壊されてしまいます。

g_variable = 100		-- グローバル変数
print(g_variable)

do
   local l_variable = "hoge"	-- ローカル変数
   print(l_variable)
end

print(g_variable)
print(l_variable)               -- nilになる

f:id:tercel_s:20130421221518p:plain

多重代入

Luaには多重代入が可能な性質があります。たとえば以下のように書くと、変数 a には10、b には 20、c には 30がそれぞれ代入されます。

a, b, c = 10, 20, 30			-- a ← 10, b ← 20, c ← 30

すべての値を評価後に代入処理が実行されるため、2変数の値を交換するために一時変数を確保する必要はありません。

x = 10
y = 20
print("x = "..x)
print("y = "..y)
print()

x, y = y, x			-- これで変数が入れ替わる
print("x = "..x)
print("y = "..y)

if文とfor文

構造化プログラミングの基本的な制御構文である if と for の書き方を紹介します。ここは感覚的に理解しやすいところなので、解説控えめで流したいと思います。

-- 変数の初期化
a, b, c = 10, 20, 20			-- a ← 10, b ← 20, c ← 20

-- if then end ステートメント
if b == c then print("bとcは等しい")     end
if a ~= b then print("aとbは等しくない") end
if a < b  then print("aはbより小さい")   end

-- forステートメント
for i = 1, 3 do 
   print(i) 
end

f:id:tercel_s:20130421213333p:plain

波括弧言語になじんでいると、構文の微妙な違いに戸惑うかも知れません。~= は見慣れない比較演算子ですが、C++!= に相当します。なんで統一しないんだ。

なお、C++&&||演算子は、Luaではandorになります。なんで統一しないんだ。

ちなみに、Luaでは条件式を短絡評価するため、この性質を利用して変数の値の設定を以下のように書くことができます。

x = 100

x = x or 0			-- 変数 x が非 nil なら x、nil なら 0 を設定
y = y or 0			-- 変数 y が(以下略

print("x = ", x)		-- 出力結果: x = 100
print("y = ", y)		-- 出力結果: y = 0

f:id:tercel_s:20130421222541p:plain
これは、変数の設定時に nil が混ざらないようにする比較的有名なテクニックです。

データ構造

Luaは、最も強力かつ唯一のデータ構造として「テーブル」をサポートしています。

テーブルの正体は単なる連想配列(辞書)なので、C++ の std::map や C# の Dictionary に近いものと考えればよいでしょう。ただし Lua の場合、任意の異なる型の情報を一つのテーブルに格納できるため、他言語における連想配列や辞書と必ずしも同一ではありません。

配列

テーブルを配列として使用することができます。ただし、配列のインデックスは1オリジンなので注意が必要です。

-- 配列の初期化
array = { "Hello", "I", "am", "Tercel" }

-- Luaの配列は1から始まります
-- for文はこんな感じで書きます
for i = 1, 4 do
   print(i.." : "..array[i])
end
print("")

-- 配列の要素数は、「#配列名」で取得できます
for i = 1, #array do
   print(i.." : "..array[i])
end
print("")

-- ちなみにforeachに相当する列挙構文もあります
for i, v in ipairs(array) do
   print(i.." : "..v)
end
print("")

3種類ほどサンプルを書きましたが、いずれも配列のインデックスと要素を列挙します。
f:id:tercel_s:20130421110720p:plain

多次元配列

配列の要素に配列をセットすることで、多次元配列を再現できます。

matrix = {}			-- 行列の生成
for i = 1, 10 do
   matrix[i] = {}		-- 行の作成
   for j = 1, 10 do
      matrix[i][j]  = 0
   end
end

連想配列・構造体

連想配列は、テーブルにキーと値をペアを格納する事で再現できます。構文は、テーブル名["キー"] = 値です。

なお、テーブル名["キー"]は、テーブル名.キーとも書けるので、C言語の構造体のような感覚でデータ操作を行うこともできます。以下のコードをご覧ください。

profile = {}			-- 空のテーブルを作成

profile["name"] = "tercel"	-- キーと値を指定
profile["age"] = 25
profile["sex"] = "male"

print(profile.name)		-- テーブル名["キー"] は、テーブル名.キー とも書ける
print(profile.age)
print(profile.sex)

f:id:tercel_s:20130421113256p:plain

関数

関数は、functionendの間に記述します。

-- 受け取った文字列を表示する関数
function printString(str)
   print(str)
end

printString("Hello World")

余談ですが、Luaの関数は複数の戻り値を返すこともできます。

-- 複数の戻り値
function sampleFunction()
   return 1, 2, 3
end

a, b, c = sampleFunction()

print("a = ", a)
print("b = ", b)
print("c = ", c)

f:id:tercel_s:20130421220141p:plain

なお、Lua変数に関数を代入できるため、上記コードは以下のように書くこともできます。

sampleFunction = function()
   return 1, 2, 3
end

a, b, c = sampleFunction()

print("a = ", a)
print("b = ", b)
print("c = ", c)

こちらは無名関数を定義し、それを変数 sampleFunction に代入しています。同様に、テーブルの要素にも関数をセットすることが可能です。


さて……

ここまで読んで下さった方に悲しいお知らせをしますが、この辺から急激に難易度が上がります。内容的にも高度なトピックであることに加えて、他言語で培った知識が今ひとつ役に立たないため、ここまで順調に理解できたとしてもこの先で沈む可能性があります。

それでは怒濤のラスト2章、メタテーブルとクラスをご覧ください。

メタテーブル

Luaにはメタテーブルと呼ばれる特殊なテーブルがあります。ざっくり言うと演算子のオーバーロードに相当する仕組みです。

一つ例を示しましょう。

-- はじめに、ベクトル(のようなもの)を作ります
vector1 = {  10,  20,  30 }
vector2 = { 100, 200, 300 }


-- メタテーブルの定義
-- ここでは、「+」演算子の再定義に相当する処理を行っています
metatable = {
   __add = function(lhs, rhs)
      local result = {}
      for i = 1, math.max(#lhs, #rhs) do
	 value1 = lhs[i] or 0	-- lhs[i] が nil なら 0
	 value2 = rhs[i] or 0	-- rhs[i] が nil なら 0
	 result[i] = value1 + value2
      end
      return result
   end
}


-- vector1, vector2にメタテーブルをセットします
-- これにより、vector1, vector2の足し算ができるようになります
setmetatable(vector1, metatable)
setmetatable(vector2, metatable)


-- 実際にやってみましょう
vector1 = vector1 + vector2


-- 結果を出力します
for i = 1, #vector1 do
   print(vector1[i])
end

これを実行すると、結果はこんな感じになります。
f:id:tercel_s:20130421131616p:plain
メタテーブルに登録できるキーのうち、比較的よく使いそうなものを独断と偏見で選んで以下にまとめました。

キー 意味 キー 意味
__add + (二項演算子 __index テーブル要素の参照
__sub - (二項演算子 __newindex テーブル要素の追加
__mul * (二項演算子 __tostring テーブルの文字列表現
__div / (二項演算子
__mod % (二項演算子
__eq == (比較演算子
__lt < (比較演算子
__le <= (比較演算子

委譲

また、__indexが定義されたメタテーブルを任意のテーブルにセットすると、そのテーブルの挙動が少し変わります。

テーブルに存在しない項目にアクセスしようとしたときLuaインタプリタは __index の検索を試みるようになります。百聞は一見に如かずと言いますので、簡単なサンプルをお見せします。

table1 = { x = 100 }
table2 = {}			-- からっぽのテーブル

-- メタテーブルをセット				   
setmetatable(table2, {__index = table1})

-- これで、table2 に存在しない要素を、table1 から探せるようになる
print(table2.x)

実行結果を見ると、 x という要素は table2 には存在しないにもかかわらず、表示されていることがわかりますね。table2 に存在しない要素を探すために、__index を介して table1 を参照しているのです。
f:id:tercel_s:20130421140838p:plain
これが何の役に立つのでしょうか? 実は、Luaオブジェクト指向的に活用するための強力な仕組みなのです。

クラス

先ほどのメタテーブルを使うと、テーブルをクラスのように利用できるようになります。ここでは簡単なクラスを作ってみます。

これから作ろうとしているものがイメージしやすいよう、Lua で作る前に C++ で書いてみました。C++ だけどポインタ使ってないからこわくないよ。

#include <iostream>

// こんなクラスです。
class MyClass {
private:
    std::string str;        // メンバ変数
    
public:
    MyClass() : str("") {}  // コンストラクタ
    virtual ~MyClass() {}   // デストラクタ
    
    virtual void setString(std::string string) {
        str = string;
    }
    
    virtual void printString() const {
        std::cout << str << std::endl;
    }
};

// こんなふうにつかいます。
int main(int argc, const char * argv[])
{
    MyClass obj1;
    obj1.setString("Hello");
    obj1.printString();
    
    MyClass obj2;
    obj2.setString("I am Tercel");
    obj2.printString();
    
    return 0;
}

いかがでしょう。そこまで複雑なことはしていないので、コードを読めばだいたい何をしているのかお解りいただけると思います。

これを Lua で書くには、メタテーブルの助けが必要です。

-- ---------
-- クラス定義
-- ---------
MyClass = {}

-- 自称コンストラクタ
MyClass.new = function()
   local instance = {}
   instance.str = ""		               -- メンバ変数を初期化
   setmetatable(instance, {__index = MyClass}) -- メタテーブルをセット
   return instance
end

-- メンバ関数(もどき)
MyClass.setString = function(instance, string)
   instance.str = string
end

MyClass.printString = function(instance)
   print(instance.str)
end

-- クラスを使ってみよう
obj1 = MyClass.new()
obj1.setString(obj1, "Hello")
obj1.printString(obj1)

obj2 = MyClass.new()
obj2.setString(obj2, "I'm Tercel")
obj2.printString(obj2)

メンバ関数の第一引数にインスタンス自身を渡しているのは、その関数がどのインスタンスから呼ばれたのかを識別する必要があるからです。C++でも、裏では似たような仕組みが動いており、単にプログラマが意識せずに済んでいるだけに過ぎません。

なお、Luaには糖衣構文があり、上記のスクリプトは以下のように書き換えることができます。これでメンバ関数にわざわざ自分自身を引き渡す必要がなくなります。

-- ---------
-- クラス定義
-- ---------
MyClass = {}

-- 自称コンストラクタ
function MyClass:new()
   local instance = {}
   instance.str = ""		               -- メンバ変数を初期化
   setmetatable(instance, {__index = MyClass}) -- メタテーブルをセット
   return instance
end

-- メンバ関数(もどき)
function MyClass:setString(string)
   self.str = string
end

function MyClass:printString()
   print(self.str)
end

-- クラスを使ってみよう
obj1 = MyClass:new()
obj1:setString("Hello")
obj1:printString()

obj2 = MyClass:new()
obj2:setString("I'm Tercel")
obj2:printString()

ちなみにメンバ関数の中では、メンバ変数は self を介してアクセスします。self とは、C++ の this ポインタに相当するものです。

実行してみましょう。最後なのでcatのおまけつき。
f:id:tercel_s:20130421204300p:plain

だいぶ長くなってしまったので、今日はここまで。

Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.