🧪

IEx.Introspection.decompose/2 を追う

2022/12/31に公開

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つのコマンドの中で同じように呼ばれています。

https://github.com/elixir-lang/elixir/blob/v1.14.2/lib/iex/lib/iex/helpers.ex#L299-L303
https://github.com/elixir-lang/elixir/blob/v1.14.2/lib/iex/lib/iex/helpers.ex#L328-L332
https://github.com/elixir-lang/elixir/blob/v1.14.2/lib/iex/lib/iex/helpers.ex#L347-L351
https://github.com/elixir-lang/elixir/blob/v1.14.2/lib/iex/lib/iex/helpers.ex#L372-L376

この関数はどんな処理をやっているのでしょうか?
この記事では、IEx.Introspection.decompose/2の仕事を追い掛けてみます。

IEx.Introspection.decompose/2 の中身

ソースコードはこちらです。
https://github.com/elixir-lang/elixir/blob/v1.14.2/lib/iex/lib/iex/introspection.ex#L30-L53

関数名のdecomposeには「~を分解する」「~を腐敗させる」といった意味があるようです。
ここから、引数の情報を分解する関数だと推測されます。

Decomposes an introspection call into {mod, fun, arity},
 {mod, fun} or mod.

ExDocのコメントを読んで見ると、関数は与えられた引数をモジュールや関数名、アリティを含んだ3つのパターンに分解して返すようです。

仮引数名はcallcontextの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をトレースしてみます。
https://github.com/elixir-lang/elixir/blob/v1.14.2/lib/iex/lib/iex/helpers.ex#L328-L332

  # 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さんが分かりやすい記事を書かれています!
ありがとうございます!
https://qiita.com/mnishiguchi/items/deddf4836593568f4c9a#__caller__0

Macro.decompose_call/1

続いて、IEx.Introspection.decompose(System.build_info(), __CALLER__)が呼び出されたとして、関数の中身をトレースしてみます。
https://github.com/elixir-lang/elixir/blob/v1.14.2/lib/iex/lib/iex/introspection.ex#L30-L31

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式のどの結果が採用されるのか、条件部分だけ抜き出したファイルを定義して実行して確かめます。

mydecompose.exs
     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