TL;DR
Vimユーザーなら、luaが設定ファイルやプラグイン開発で使われていることはご存知かと思います。
ただ、luaはもっと広い分野で使われており、ゲーム開発や組み込みシステムなどでも利用されています。
本記事では、luaの基本的な特徴と、特にtable、function、coroutineの3つのデータ型に焦点を当てて解説します。
Lua: documentation から遷移できる
A Look at the Design of Lua – Communications of the ACM に特徴がまとめられています。
本記事はこちらの内容を元として、私なりに解説を加えたものです。
luaとは?
1993年に生まれたスクリプト言語で、XMLやJavaScriptが登場する前から存在しています。
luaの主なポイントは次の三つです
- 言語としての目標:シンプルさ(Simplicity)、移植性(Portability)、組み込み性(Embeddability)
- Luaは2万5000行のC言語で実装され、バイナリサイズとして200KiB程度
- 他の言語との相互運用性(Interoperability)が高い
加えて、luaのモットーは「方針よりもメカニズム」です。
例えば、ISO C言語はmoduleやinterfaceの概念はないですが、file includeやexternal declarationで実現できます。
この考え方は、luaの設計にも反映されていて、DYI(Do-It-Yourself)のアプローチを用いてmoduleやオブジェクト指向プログラミングを実現しています。
luaは8つのデータ型をサポートしていて、特に最後の3つはluaの特徴的な部分です。
- nil
- boolean
- number
- string
- userdata
- table
- function
- thread (coroutine)
table
これは、配列やハッシュテーブルのようなデータ構造の両方を持つことができるデータ型です。
記述内容に応じて、配列やハッシュテーブルとして振る舞います。
例えば、次のように書くと、配列として振る舞います。
t = {3, 2, 1}
print(t[1]) -- 出力: 3
一方、次のように書くと、ハッシュテーブルとして振る舞います。
t = {name = "Alice", age = 30}
print(t["name"]) -- 出力: Alice
内部構造としては、Tableはarrayとhashの二つの構成からなると説明されています。
The internal representation of a table in Lua has two parts: an array and a hash.
これは私の予想でしかないですが、実装内容に応じてどちらの部分を使うかを決定しているのではないかと思います。
function
luaにおいて関数はfirst-classです。つまり、関数を変数に代入したり、引数として渡したり、戻り値として返すことができます。
例えば、次のように書くことができます。
add = function (x, y)
return x + y
end
result = add(2, 3) -- 関数を呼び出す
print(result) -- 出力: 5
tableとfunctionを用いた応用
moduleの実現
luaではtableとfunctionを組み合わせることで、moduleを実現できます。例えば、次のように書くことができます。
-- mymodule.lua
local M = {}
function M.greet(name)
return "Hello, " .. name .. "!"
end
return M
-- main.lua
local mymodule = require("mymodule") -- ここで mymoduleはtableを返す
print(mymodule.greet("World")) -- 出力: Hello, World!
このように、luaのシンプルなデータ型と関数の特性を活かして、柔軟なプログラム構造を作ることができます。
個人的に、上の話を聞いてもなんの恩恵があるのかピンと来なかったのですが、記事内で次のように説明されています。
Finally, this design is friendly to the C API. All it needs is basic manipulation of tables and functions, plus the standard function setmetatable.
つまり、C言語からluaを操作する際に、tableとfunctionさえあれば十分であり、他の複雑な概念を導入する必要がないということです。
これは、luaの設計目標である「他の言語との相互運用性(Interoperability)が高い」にもつながっていると感じました。
オブジェクト指向プログラミングの実現
metatableやmetamethod(__index)を用いることで、クラスに対しての演算及び継承を実現できます。
特に、__index metamethodは、テーブルに存在しないキーにアクセスした際に呼び出されるメソッドで、委譲(delegation)を実現するために使用されます。
以下では、継承の例を示します(演算については、参照ドキュメントに記載があり__add metamethodなどを利用することで実現可能です)。
-- クラスの定義
Person = {}
function Person:new(name, age)
local obj = {name = name, age = age}
return obj
end
function Person:greet()
return "Hello, my name is " .. self.name .. " and I am " .. self.age .. " years old."
end
function Person:showAge()
return self.age
end
-- サブクラスの定義
Student = setmetatable({}, {__index = Person}) -- Personを継承
Student.__index = Student
function Student:new(name, age, studentID)
local obj = Person.new(self, name, age) -- 親クラスのコンストラクタを呼び出す
obj.studentID = studentID
setmetatable(obj, Student)
return obj
end
function Student:greet()
return Person.greet(self) .. " I am also a student with ID " .. self.studentID .. "."
end
-- オブジェクトの生成とメソッドの呼び出し
local alice = Student:new("Alice", 20, "S12345")
print(alice:greet()) -- 出力: Hello, my name is Alice and I am 20 years old. I am also a student with ID S12345.
print("Age: " .. alice:showAge()) -- 出力: Age: 20
Coroutine
luaのcoroutineは、並行処理を実現するための軽量なスレッドのようなもので、次の特性を持っています。
- first-class: コルーチンは変数に代入したり、引数として渡したり、戻り値として返すことができます。
- 実行の中断と再開: コルーチンごとに独自のコールスタックを持つため、実行を中断(suspend)して後で再開(resume)することができます。
- 非対称性: 対称的なコルーチンではコルーチンに対して一つの操作しかできませんが、luaのコルーチンは非対称であり
resumeとyieldの二つの操作が可能で、これはcall-returnの関係に似ています。
他にも one-shot continuationsに似ているとありました。ただ、内容がイマイチ理解できなかったので、興味がある方は元記事を参照してください。
Despite this equivalence, coroutines offer one-shot continuations in a format that is more natural for a procedural language due to its similarity to multithreading.
co = coroutine.create(function ()
for i = 1, 3 do
print("Coroutine iteration: " .. i)
coroutine.yield() -- 実行を中断
end
end)
coroutine.resume(co) -- 出力: Coroutine iteration: 1
coroutine.resume(co) -- 出力: Coroutine iteration: 2
coroutine.resume(co) -- 出力: Coroutine iteration: 3
coroutine.resume(co) -- 何も出力されない。コルーチンは終了している
Coroutineの応用例
- 協調的マルチスレッドの実現: Gameのイベントにて各キャラクターの動作をcoroutineで管理し、イテレーションごとに状態を更新し、それぞれのcoroutineがyieldすることで、協調的に動作を切り替えることができます。
-
who-is-the-boss問題の解決: スクリプト言語を組み込むうさいに、どちらが主制御権を持つか問題が発生します。ただ、resumeとyieldを用いることで、制御権の移譲を明示的に行うことができ、問題を解決できます。
協調マルチスレッドですが、おそらく coroutine.status を用いて状態を管理することになるかと思います。
詳細は Programming in Lua : 9.1 を参照してください。
luaの適応事例
下記のリンクに、luaを利用しているアプリケーションの一覧があります。
List of applications using Lua - Wikipedia
OSSから商用ソフトウェアまで幅広く利用されていることがわかります。
個人的な感想
- luaのcoroutineですが、マルチスレッドかどうかよくわかりませんでしたが、おそらくアプリケーションレベルでシングルスレッドの中で協調的マルチスレッドを実現するための仕組みで、OSレベルのスレッドとは異なるものだと理解しました。
- coroutineについて協調的に動作を切り替えるのは面白いと感じました。特にゲーム開発と相性が良いというのも納得しました。一方で、自分で状態管理を明示的に書かないといけないので、他の言語の並行処理(例えばGoのgoroutineやJavaScriptのasync/await)と比べると、少し手間がかかるように感じました。
- tableという概念は初耳だったので驚きました。ただ内部的にhashとarrayの二つの構成があるのであれば、プログラマーが明示的に意識しても良いのではないかと思いました。
- moduleについてもtableとfirst-classの内容をうまく組み合わせているのは非常に面白いなと感じました。