はじめに
最近は Neovim のプラグインを Lua で書くことが多いです。
全く Lua を知らない状態からそれなりに上達できた気がするので、学んできたことをダンプしておこうと思います。
免責事項
この記事では、章立てを整理したりせず、学んできたことをトピックベースで雑多に並べて記載します。
記事を読む方に、なにか一つでも刺されば幸いです。
また、自分はプロダクション環境で Lua を書いているわけではないので、Lua を業務で利用している人からしたら「それはおかしい」というものが混ざっているかもしれません。
あくまでも、これまで自分がプラグインを開発してきた中で獲得してきた現在のナレッジだと捉えてください。
トピック
コーディングスタイル
割とみんな好き勝手やっているようです。
参考にするなら luvit/luv をおすすめします。(現実的な範囲に収めつつ、stdlib との親和性も意識しているので)
自分は Roblox Lua のスタイルが好きなのでこれに影響を受けたコーディングスタイルを採用しています。
(クラス名以外は snake_case を採用しているので、厳密には Ruby のようなスタイルになってしまっていますが)
-
- 特徴
- クラスを表す識別子は PascalCase とする
- 変数名は camelCase とする
- ガッツリ OOP(ゲーム向けだからってのもあるのかな?)
- 感想
- かなり好み
- Prototype ベースで OOP やってた頃の JavaScript を想起させる
- Lua 界隈では異端
- 特徴
-
- 特徴
- 識別子は snake_case
- クラスがほぼ登場しないので、ほぼ全部 snake_case
- あまり OOP を意識したコードではない
- 識別子は snake_case
- 感想
- Lua 界隈ではそこそこスタンダードな意思決定に思える
- 特徴
-
- 特徴
- 識別子は snake_case
- 少なくともユーザランドではそうなっている(内部は不明)
- OOP とそれ以外を両立するスタイル
- こうもかけるし
module.new_a():do_b()
- こうもかける
module.do_b(module.new_a())
- こうもかけるし
- 識別子は snake_case
- 感想
- おそらく Lua stdlib をかなり意識している
- 特徴
-
Lua stdlib
- 特徴
- 識別子は lowercase ...?
-
_
を登場させないぞ、という気概を感じる
-
- OOP とそれ以外を両立するスタイル
- こうもかけるし
('string'):gsub('pat', 'rep')
- こうもかける
string.gsub('string', 'pat', 'rep')
- こうもかけるし
- 識別子は lowercase ...?
- 感想
- 多分これが Lua 界隈でのデファクトでしょう
- とはいえ、lowercase 強制は辛い
- 特徴
OOP のイディオム
Lua では OOP をやる場合のほぼ決まりきったイディオムが存在するので、覚えておくべきです。JavaScript のプロトタイプベースの継承チェーンを理解しているならイメージが付くでしょう。
local Person = {}
Person.__index = Person
function Person.new(first_name, last_name)
local self = setmetatable({}, Person)
self.first_name = first_name
self.last_name = last_name
return self
end
function Person:get_name()
return self.first_name .. self.last_name
end
return Person
-- 「継承は?」と言われそうですが、最近は継承させてないのでまだ調べてないです。
-- たぶん `Person.__index = setmetatable(Person, Parent)` でいけるかなと想像
metatable
ライブラリを開発していて、ちょっと気の利いた API を提供しようと思うとメタプログラミングのような機能が欲しくなったりします。Lua ではかなり強力な metatable という機能が提供されているので、Lua でガッツリコードを書いていくのであれば覚えておくべきです。
-- GC の検出
local gc_detection = newproxy()
getmetatable(gc_detection).__gc = function()
print('gc occurred.')
end
gc_detection = nil
collectgarbage('collect') -- print 'gc occurred.'
-- 関数(のようなもの)に属性をもたせる
local function memoize(func)
return setmetatable({
cache = cache.new(),
clear = function(self)
self.cache = cache.new()
end
}, {
__call = function(self, ...)
if self.cache:has(...) then
return unpack(self.cache:get(...s))
end
local res = { func(...) }
self.cache:set({ ... }, res)
return unpack(res)
end
})
end
-- 他にも、数値演算などの振る舞いを変更することもでき、多彩です。
-- React ステート管理界隈が大喜びしそうな `__eq` も搭載されています。
-- http://lua-users.org/wiki/MetatableEvents
言語サーバが優秀
Lua には sumneko/lua-language-server が存在しており、この言語サーバはなんとコメントの型指定アノテーションを記載することで 型チェック までできてしまいます。
---@enum namespace.async.Status
local Status = {
Pending = 1,
Fullfilled = 2,
Rejected = 3
}
---@generic T: any[]
---@param self T
---@return T
function reverse(self)
local reversed = {}
for i = #self, 1, -1 do
table.insert(reversed, self[i])
end
return reversed
end
-- ジェネリクス周りの対応は甘かったりしますが、関数の引数程度であればバッチリです。
-- 個人開発のプロジェクトということを考えれば、信じられない高機能さだと思います。
-- 型チェックができるだけではなく、補完やリネームの参照情報などにも利用されます。
想像よりなんでもできるということ
Lua という言語は、足回りは固めてくれています。
- OOP や、メタプログラミング全般に使える metatable
- OOP が書きやすくなるような
:
関数呼び出しの syntax sugar - 関数自体をバイナリ化して持ち運べる
string.dump
-
loadstring
/loadfile
などの動的な Lua コードの読み込み -
packages
変数によるモジュールロードへの介入 - ジェネレータ関数(coroutine)
- やたらかんたんに使える ffi モジュール
という感じで「お前が頑張ってコードを書けば大体のことはできる状態を作ってやる」という気概が感じられます。(でも、本当に「便利ではない」のが辛い。標準ライブラリが貧弱すぎる)
終わりに
書き始めてみたけど、意外と量がでてこなかったです。
次は Neovim の文脈を取り入れた、最近の Lua 活について書こうかなと思います。