/var/log/messages

Feb 11, 2019 - 2 minute read - Comments - programming

Metaprogramming Elixir (2)

マクロは caller のためのコードを生成するのではなく注入 (inject) する、とあります。コンテキストに注入、という表現になっていますね。コンテキストとは caller の

  • bindings
  • imports
  • aliases

とあります。

二つのコンテキスト

macros/callers_context.exs 作成してみます。以下。

defmodule Mod do
  defmacro definfo do
    IO.puts "In macro's context (#{__MODULE__})."

    quote do
      IO.puts "In caller's context (#{__MODULE__})."

      def friendly_info do
        IO.puts """
        My name is #{__MODULE__}
        My functions are #{inspect __info__(:functions)}
        """
      end
    end
  end
end

defmodule MyModule do
  require Mod
  Mod.definfo
end

typo な箇所がわからず compilation error が取れなくて、な挙句の実行例が以下です。

iex(1)> c "callers_context.exs"
warning: redefining module Mod (current version defined in memory)
  callers_context.exs:1

In macro's context (Elixir.Mod).
In caller's context (Elixir.MyModule).
[MyModule, Mod]
iex(2)> MyModule.friendly_info
My name is Elixir.MyModule
My functions are [friendly_info: 0]

:ok

これだけで色々なことがわかって面白いです。

  • defmacro は翻訳時に eval されている
  • quote は翻訳時には eval されていない
  • 翻訳時にマクロの呼び出し (展開) が eval されている
  • Mod.definfo マクロ呼び出し (展開) により quote が eval されている

あるいは、definfo マクロの呼び出し (というか展開) で定義された手続きは MyModule モジュールの、という形になっていることもわかります。

Hygiene

以下を呼び出し元には leak しない、とのこと。

  • variables
  • imports
  • aliases

以下なナニが例示されています。

iex(3)> ast = quote do
...(3)> if meaning_to_life == 42 do
...(3)> "it's true"
...(3)> else
...(3)> "it remains to be seen"
...(3)> end
...(3)> end
{:if, [context: Elixir, import: Kernel],
 [
   {:==, [context: Elixir, import: Kernel],
    [{:meaning_to_life, [], Elixir}, 42]},
   [do: "it's true", else: "it remains to be seen"]
 ]}
iex(4)> Code.eval_quoted ast, meaning_to_life: 42
warning: variable "meaning_to_life" does not exist and is being expanded to "meaning_to_life()", please use parentheses to remove the ambiguity or change the variable name
  nofile:1

** (CompileError) nofile:1: undefined function meaning_to_life/0
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (elixir) expanding macro: Kernel.if/2
    nofile:1: (file)

quote の中で使われている変数にはアクセスできません、と。ただし、override しちゃうことも可能ですよ、というソレも例示されています。

iex(4)> ast = quote do
...(4)> if var!(meaning_to_life) == 42 do
...(4)> "it's true"
...(4)> else
...(4)> "it remains to be seen"
...(4)> end
...(4)> end
{:if, [context: Elixir, import: Kernel],
 [
   {:==, [context: Elixir, import: Kernel],
    [
      {:var!, [context: Elixir, import: Kernel],
       [{:meaning_to_life, [], Elixir}]},
      42
    ]},
   [do: "it's true", else: "it remains to be seen"]
 ]}
iex(5)> Code.eval_quoted ast, meaning_to_life: 42
{"it's true", [meaning_to_life: 42]}

手続きを書いてみて確認。setter1.exs の定義が以下です。

defmodule Setter do
  defmacro bind_name(string) do
    quote do
      name = unquote(string)
    end
  end
end

ここでは var! で公開していないのでアクセス不可能というかカプセル化されています。

iex(6)> c "setter1.exs"
[Setter]
iex(7)> require Setter
Setter
iex(8)> name = "Chris"
"Chris"
iex(9)> Setter.bind_name("Max")
"Max"
iex(10)> name
"Chris"

もうひとつは var! を使う例です。

defmodule Setter do
  defmacro bind_name(string) do
    quote do
      var!(name) = unquote(string)
    end
  end
end

これ、iex を再起動しないと駄目だったのですが何か方法があるのかしら。

iex(1)> c "setter2.exs"
[Setter]
iex(2)> require Setter
Setter
iex(3)> name = "Chris"
"Chris"
iex(4)> Setter.bind_name("Max")
"Max"
iex(5)> name
"Max"

ようやく 1 章を読了な模様。

Metaprogramming Elixir (1) Metaprogramming Elixir (3)

comments powered by Disqus