どうも意味分からんタイトルですみません。でもこのタイトルのことをやるのがこの記事の目的です。
packer.nvim でよく書くこれ
packer.nvim でプラグインを追加する際、setup
関数を設定の度に呼び出すことがよくあります。
use {
"rcarriga/nvim-notify",
config = function()
require("notify").setup {}
end,
}
こういうのしょっちゅうあると思うんですが、なんかいちいち function() …… end
で囲むのダルくないですか? function
って文字数多いしイヤなんですよね。
use {
"rcarriga/nvim-notify",
config = lazy_require("notify").setup {},
}
こう書けたらスマートですよね。これができるようになるプラグインを作りました。
ふーん……で、何がすごいの?
いや意外と難しいんですよ。これ。理由は packer.nvim の「コンパイル」機能にあります。例えば上記の rcarriga/nvim-notify のための設定は PackerCompile
コマンドによって以下のようなコードに「コンパイル」されます。
["nvim-notify"] = {
config = { "\27LJ\2\n8\0\0\3\0\3\0\a6\0\0\0'\2\1\0B\0\2\0029\0\2\0004\2\0\0B\0\2\1K\0\1\0\nsetup\vnotify\frequire\0" },
loaded = false,
needs_bufread = false,
only_cond = false,
path = "/Users/jinnouchi.yasushi/.local/share/nvim/site/pack/packer/opt/nvim-notify",
url = "https://github.com/rcarriga/nvim-notify"
},
config
関数が訳分かんない文字列になってますね。これバイトコードなんです。packery.nvim は高速化のため、ユーザーの設定を中間表現にコンパイルして保持し、Neovim 起動時には高速に読み込んでくれるんですね。
Lua でコードを中間表現に変換し、それをまた実行するには次のようにします。
local function print_hoge()
print "hogehogeo"
end
-- 中間表現に変換
local byte_code = string.dump(print_hoge)
-- もう一度関数に変換
local hoge_fn = assert(loadstring(byte_code))
hoge_fn()
これをファイルに保存して実行すると hogehogeo
と表示されます。ここでもう一度上述の設定行を見てみます。
config = function()
require("notify").setup {}
end,
packer.nvim は config
に関数が指定されるとこれを string.dump
でバイト列に変換し、Neovim 実行時は逆に loadstring
で関数に変換しているのですね。
任意のモジュールを require
して結果を返す関数を返す関数
まずはこれを書かないといけません。どういうことかと言うと、packer.nvim でプラグインを追加している部分では生の require "notify"
を書いてしまうと notify
モジュール自体を表してしまいます。欲しいのは実行時に notify
モジュールを返してくれる関数であって、notify
モジュール自体ではありません。
function lazy_require(module)
return function()
require(module)
end
end
lazy_require "notify" --> notify モジュールを返す関数
lazy_require "notify" () --> notify モジュール
となっていい感じです。
任意のモジュールの関数を呼び出せるようにする
しかし、最終的にやりたいのはモジュール自体を手に入れることではなく、モジュールの setup
関数を呼び出したい、ということでした。
function lazy_require(module)
return function()
return {
setup = function(opts)
return function()
require(module).setup(opts)
end
end,
}
end
end
lazy_require("notify").setup {} --> require("notify").setup {} を行う関数
setup
以外の任意の関数を呼び出せるようにするなら、ちょっと複雑ですがメタテーブルを使えば良いです。
function lazy_require(module)
return function()
return setmetatable({}, {
__index = function(_, method)
return function(opts)
return function()
require(module)[method](opts)
end
end
end,
})
end
end
lazy_require("notify").setup {} --> require("notify").setup {} を行う関数
lazy_require("notify").history(1) --> require("notify").history(1) を行う関数
できましたね。これで完成!……ではないんですね。
loadstring
するとクロージャは使えない
そうなんです。loadstring
で作成された関数はあくまでコード自体を中間表現にしたものを表しているだけで、呼び出された時のコンテキストは保持していません。上記の実装の場合、
config = lazy_require("notify").setup {}
これが packer.nvim によって string.dump
→ loadstring
されると、以下のような意味になってしまいます。
-- 以下は疑似コードです。
config = function()
require(module)[method](opts)
end
module
, method
, opts
のような、関数外で定義された変数は参照できないのですね。つまり、クロージャが扱えないんです。
これをどう解決するのか悩んだんですが、クロージャをただの関数にしてしまうことにします。どうするかと言えば、変数の値を含め、バイナリ(中間表現)にして保持してしまうのです。
クロージャを中間表現で表す
先ずは、lazy_require "notify"
の部分を考えます。
function lazy_require(module)
return require(module)
end
こういうやつですね。これを中間表現にする関数は以下のように書けます。
local function byte_code_require(module)
local f = assert(loadstring(('return require "%s"'):format(module)))
return string.dump(f)
end
byte_code_require "notify"
などとすると、require "notify"
の結果を返す関数、を中間表現にして返してくれます。実際に見てみると次のようなバイナリです。
local byte_code = byte_code_require "notify"
print(vim.inspect(byte_code))
--> "\27LJ\2\b\23return require \"notify\")\2\0\3\0\2\0\3\4\0\0016\0\0\0'\2\1\0D\0\2\0\vnotify\frequire\1\1\1\0\0"
--> と表示されます。
バイナリの中に return require \"notify\"
のような文字列があり、引数として指定したモジュール名が埋め込まれていることが分かりますね。これなら loadstring
してもコンテキストが再現可能です。
次に、lazy_require("notify").setup {}
のように、任意のプロパティの関数を呼び出す関数の中間表現を返してみましょう。
local function index(self, method)
return function(opts)
local f_str = ([[
local module = assert(loadstring(%s))()
local opts = assert(loadstring(%s))()
module.%s(opts)
]]):format(vim.inspect(self.load_module), vim.inspect(serialize(opts)), method)
return assert(loadstring(f_str))
end
end
return function(module)
return setmetatable({
load_module = byte_code_require(module),
}, {
__index = index,
})
end
ううむ。一気に難しくなりましたね。まず、ここではメタテーブルの __index
に関数をセットし、プロパティを参照した時の動作を定義しています。関数 index()
には self
、つまりそのテーブル自身と、method
、参照しようとした関数名が渡されます。例えば、lazy_require("notify").setup {}
という記述は、
変数 | 内容 |
---|---|
self |
{ load_module = byte_code_require "notify" } |
method |
"setup" |
opts |
{} |
のようになる訳です。関数の中では Lua のコードを文字列で作り、それを中間表現で返しています。
ここで、中間表現のバイナリを実行し、結果を得る方法をおさらいしておきましょう。あるバイナリが変数 foo
に入っている時、そのコードを実行して結果を得るには次のような構文を使います。
local result = assert(loadstring(foo))()
上述の index()
では次のようにコードを構成していますが、
local module = assert(loadstring(%s))()
local opts = assert(loadstring(%s))()
%s
にはそれぞれ byte_code_require "notify"
の返り値と、opts
をシリアライズした文字列が入ります。これによりモジュール本体と、最終的に渡すオプション opts
が手に入ります。そして最後に、
module.%s(opts)
のようにして呼び出しているのですね。%s
には関数の名前、つまり今回の場合は "setup"
が入ります。これで求めるものが手に入りました。
「シリアライズ」ってサラッと書きましたが、Lua ではこれも中々熱いテーマです。何しろ言語として提供されている機能ではないので様々な実装が存在します。今回は Tony Finch さんの実装を使いました。同種の実装・ライブラリは沢山ありますので以下のページを参照してください。
こんなに苦労したけど……
個人的には結構複雑なことやった気がするんですが、結果として得られたのは、
config = function()
require("notify").setup {}
end
これを、
config = lazy_require("notify").setup {}
こう書けるようになっただけです。それでも苦労したことを書いとかないと多分自分でも忘れちゃうのでここに纏めておきます。
こんなに苦労する原因は、packer.nvim が config
オプションに与えられた関数を「コンパイル」してしまうからです。このような分かりにくさを嫌ってか、packer.nvim の次期バージョンは「コンパイル機能」を全く使わないものになる予定です。現在リポジトリの main
ブランチで開発されているので参照してください。
こっちも見てね!
今回は Advent Calendar にもう 1 個参加してます。この記事はネタみたいなものですが、↓はかなり本気で書いたので是非読んでください。