Neovim Lua用のPromiseを実装した
NeovimのLua製プラグインで使えるPromiseを実装した。
なぜ?
世にPromiseのLua実装は幾つかあるが、
Neovimで使う際に欲しい機能が少しずつ足りなかったので実装した。
以下でNeovimやLuaに特有な点を紹介する。
Neovimのevent-loopで非同期実行する
JavaScriptのPromise.then
は常に非同期で実行される。
Promise.resolve().then(function() {
console.log(2);
});
console.log(1);
// 1
// 2
promise.nvimではこの非同期実行のためにvim.schedule()
を使っている。
vim.schedule()
に渡された関数はNeovimのmain event-loopで実行されるようにキューに入る。
vim.schedule(function()
print(2)
end)
print(1)
-- 1
-- 2
Unhandled Rejectionを検知する
rejectされたpromiseがcatchされない場合、エラーを握りつぶされるとデバッグが困難になる。
JavaScriptの実行環境のように何かしら検知する手段が欲しい。
promise.nvimではpromiseがGCに回収される際にUnhandled Rejectionを判定するようにした。
Neovim LuaはLuaJIT(Lua 5.1 compatible)なのでtable
についてはGC時に__gc
が呼ばれない。
よってuserdata
の__gc
を使うハックが必要になる。
以下に簡略化したコードを示す。
do
-- promiseのstatus, handledが最終的に以下になったとする
local promise = {status = "rejected", handled = false}
-- 空のuserdataを作るハック http://lua-users.org/wiki/HiddenFeatures
local userdata = newproxy(true)
getmetatable(userdata).__gc = function()
if promise.status == "rejected" and not promise.handled then
print("unhandled rejection")
end
end
-- https://www.lua.org/manual/5.1/manual.html#2.10.2
-- promiseへの通常の参照が消えたらuserdataがGCの対象になる
promise._userdata = setmetatable({[promise] = userdata}, {__mode = "k"})
end
collectgarbage() -- GCを明示的に動かす
-- unhandled rejection
多値を扱う
多値をresolve
, reject
できるようにした。
local Promise = require("promise")
Promise.new(function(resolve)
resolve(1, 2)
end):next(function(a, b)
return a, b
end):next(function(a, b)
print(a + b)
end)
-- 3
不要かなと迷いつつ、2値ぐらいは使いそうだったので実装した。
この機能がなくてもtable
に入れてresolve
, reject
して使う側で展開すれば足りる。
ただ、その変数に名前を付けるのが面倒な場面もあり、気楽さを優先してこうした。
Luaの書き味には合ってる気がする。
プラグインに埋め込む
今のところLua製プラグインの依存ライブラリを管理する仕組みはないので、
ユーザーに依存ライブラリを別途インストールしてもらうか、
プラグインに埋め込むしかない。
promise.nvimはライセンスをCC0にして気楽に扱えるようにしたので、
1ファイルを例えばlua/myplugin/promise.lua
にコピペすれば以下のように使える。
local Promise = require("myplugin.promise")
(もちろん手動で管理するのは辛い)
感想
ちゃんとPromiseを学んだことがなかったので JavaScript Promiseの本 がとても参考になった 🙏
Discussion