IEx.Introspection.decompose/2 を追う
intro
この記事は、「Elixir Advent Calendar 2022」カレンダー4の20日目の記事になります。
はじめに
ElixirのインタプリタIExには様々なコマンドが存在します。
iex(1)> h
(前略)
There are many other helpers available, here are some examples:
• b/1 - prints callbacks info and docs for a given module
• c/1 - compiles a file
• c/2 - compiles a file and writes bytecode to the given path
• cd/1 - changes the current directory
• clear/0 - clears the screen
• exports/1 - shows all exports (functions + macros) in a module
• flush/0 - flushes all messages sent to the shell
• h/0 - prints this help message
• h/1 - prints help for the given module, function or macro
• i/0 - prints information about the last value
• i/1 - prints information about the given term
• ls/0 - lists the contents of the current directory
• ls/1 - lists the contents of the specified directory
• open/1 - opens the source for the given module or function in
your editor
• pid/1 - creates a PID from a string
• pid/3 - creates a PID with the 3 integer arguments passed
• port/1 - creates a port from a string
• port/2 - creates a port with the 2 non-negative integers passed
• pwd/0 - prints the current working directory
• r/1 - recompiles the given module's source file
• recompile/0 - recompiles the current project
• ref/1 - creates a reference from a string
• ref/4 - creates a reference with the 4 integer arguments
passed
• runtime_info/0 - prints runtime info (versions, memory usage, stats)
• t/1 - prints the types for the given module or function
• v/0 - retrieves the last value from the history
• v/1 - retrieves the nth value from the history
(後略)
それらのコマンドの各実装の中で共通して使われているIEx.Introspection.decompose/2
という関数があります。
この関数はIExの4つのコマンドの中で同じように呼ばれています。
この関数はどんな処理をやっているのでしょうか?
この記事では、IEx.Introspection.decompose/2
の仕事を追い掛けてみます。
IEx.Introspection.decompose/2 の中身
ソースコードはこちらです。
関数名のdecompose
には「~を分解する」「~を腐敗させる」といった意味があるようです。
ここから、引数の情報を分解する関数だと推測されます。
Decomposes an introspection call into
{mod, fun, arity}
,
{mod, fun}
ormod
.
ExDocのコメントを読んで見ると、関数は与えられた引数をモジュールや関数名、アリティを含んだ3つのパターンに分解して返すようです。
仮引数名はcall
とcontext
の2つです。
call
はAST(コードの内部表現)、context
は呼び出し元の情報を期待します。
↓ ASTは以下のような構造を持ちます。
iex(1)> quote(do: MyModule.foo([1, 2, 3]))
{{:., [], [{:__aliases__, [alias: false], [:MyModule]}, :foo]}, [], [[1, 2, 3]]}
トレース
IEx.Introspection.decompose/2
IEx.Helpers.h/1
からの呼び出しを例にとって、IEx.Introspection.decompose/2
をトレースしてみます。
# IEx.Helpers.h(System.build_info()) を呼び出した例を考えてみます
# System.build_info() は文字列じゃないことに注意
defmacro h(System.build_info()) do
quote do
IEx.Introspection.h(unquote(IEx.Introspection.decompose(System.build_info(), __CALLER__)))
end
end
Elixirは関数呼び出しの際、引数が評価された結果が関数に渡されます。
ですが、マクロに渡す引数は評価されない状態のまま渡されます(前述のASTの形で渡される)
ここでは最初の引数に渡されたSystem.build_info()
は評価されずSystem.build_info()
のままIEx.Helpers.h/1
に渡されます。
渡されているSystem.build_info()
のASTは以下のような情報(のはず)
iex(1)> quote do: System.build_info()
{{:., [], [{:__aliases__, [alias: false], [:System]}, :build_info]}, [], []}
__CALLER__
__CALLER__
はマクロの中でのみ使える、呼び出し元情報を持ったMacro.Env
構造体を返すマクロです。
__CALLER__
については、@mnishiguchiさんが分かりやすい記事を書かれています!
ありがとうございます!
Macro.decompose_call/1
続いて、IEx.Introspection.decompose(System.build_info(), __CALLER__)
が呼び出されたとして、関数の中身をトレースしてみます。
IEx.Introspection.decompose/2
の中身を見ると、実行後すぐにMacro.decompose_call/1
を呼び出しています。
引数の中身で置き換えると下記のようなコードになります。
def decompose(System.build_info(), :build_info]}, [], []}, __CALLER__) do
case Macro.decompose_call(System.build_info()) do
(後略)
System.build_info()
の部分には{{:., [], [{:__aliases__, [alias: false], [:System]}, :build_info]}, [], []}
Macro.decompose_call/1
はAST(コードの内部表現)を渡して、リモート情報と関数名と引数リストを返します。
リモート情報とは、ASTの第1項目に含まれる呼び出し元の情報を指すようです。
Macro.decompose_call/1
を実行すると下記のような結果が得られます。
iex(1)> quote do: System.build_info()
{{:., [], [{:__aliases__, [alias: false], [:System]}, :build_info]}, [], []}
iex(2)> Macro.decompose_call(quote do: System.build_info())
{{:__aliases__, [alias: false], [:System]}, :build_info, []}
非常に分かりにくいのですが、ASTの真ん中の情報が取り出されるようです。
これを元にトレースを続けていきます。
IEx.Introspection.decompose/2 再び
IEx.Introspection.decompose/2
のソースコードを、さきほどのMacro.decompose_call(quote do: System.build_info())
の結果で置き換えると下記のようなコードになります。
def decompose(call, context) do
case {{:__aliases__, [alias: false], [:System]}, :build_info, []} do
{_mod, :__info__, []} ->
Macro.escape({Module, :__info__, 1})
{mod, fun, []} ->
{mod, fun}
{maybe_sigil, [_, _]} ->
case Atom.to_string(maybe_sigil) do
"sigil_" <> _ ->
{:{}, [], [find_decompose_fun_arity(maybe_sigil, 2, context), maybe_sigil, 2]}
_ ->
call
end
{fun, []} ->
{find_decompose_fun(fun, context), fun}
_ ->
call
end
end
case式のどの結果が採用されるのか、条件部分だけ抜き出したファイルを定義して実行して確かめます。
1 defmodule Test do
2 def decompose() do
3 case {{:__aliases__, [alias: false], [:System]}, :build_info, []} do
4 {_mod, :__info__, []} -> 1
5 {mod, fun, []} -> 2
6 {maybe_sigil, [_, _]} -> 3
7 {fun, []} -> 4
8 _ -> 5
9 end
10 end
11 end
12
13 IO.puts Test.decompose()
$ elixir mydecompose.exs
2
実行すると、2が出力されるため {mod, fun, []} -> {mod, fun}
にマッチしたことが分かります。
つまり、ASTからモジュール情報と関数名を返すことになります。
直接関数にASTを渡して結果を見てみると、ASTの第1要素から関数の所属するモジュール情報と関数名が返っていることが分かります。
iex(1)> IEx.Introspection.decompose({{:., [], [{:__aliases__, [alias: false], [:System]}, :build_info]}, [], []}, __ENV__)
{{:__aliases__, [alias: false], [:System]}, :build_info}
まとめ
-
IEx.Introspection.decompose/2
はASTから所属モジュール情報と関数名、アリティを返します。 - 引数として指定された文字列がどのモジュールのどの関数かを判別するのに利用することができます。
最後に、IEx.Introspection.decompose/2
をマクロの外側で使う方法を紹介します。
iex(1)> IEx.Introspection.decompose(quote do System.build_info() end, __ENV__)
{{:__aliases__, [alias: false], [:System]}, :build_info}
Discussion