/var/log/messages

Feb 17, 2019 - 3 minute read - Comments - programming

Metaprogramming Elixir (6)

ちょっと復習。accumulate:true な記述のあたりを列挙しておきます。

  • モジュール属性を使用すると、コンパイル時にデータをモジュールに格納できる
  • これらは他の言語で定数が適用される場所でよく使われるが、Elixirはコンパイル時に悪用するための他のトリックを提供する
  • 属性を登録するときにaccumulate:trueオプションを利用することで、コンパイル段階で登録された追加リストを保持できる
  • モジュールがコンパイルされた後、属性にはコンパイル中に発生したすべての登録のリストが含まれる

accumulate:true が今ひとつ理解できていないのかどうか。てか、翻訳時に評価された値が属性に値として格納される、ってことなのか。そしてここで問題になってくるのが評価される順番なのね。

ともあれ

最終版を再確認してみます。上から順に、ということで以下。

defmodule Assertion do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
      Module.register_attribute __MODULE__, :tests, accumulate: true
      @before_compile unquote(__MODULE__)
    end
  end

__using__ という macro はその module が use された時に展開されます。上記では

  • 自分自身を import して attribute を登録
  • module attribute はコンパイル時にデータを取得して保持できる
  • 登録時、accumulate:true オプション指定することでコンパイル中に発生した全ての registration を保持できる
  • @before_compile 属性は引数に __before_compile__ macro を定義する module を受け取る
  • __before_compile__ macoro はコード生成の最終段階で呼び出される

ということで Assertion モジュールでは次にその macro が定義されています。

  defmacro __before_compile__(_env) do
    quote do
      def run do
        Assertion.Test.run(@tests, __MODULE__)
      end
    end
  end

run メソドが定義され、その中では Assertion.Test.run が呼び出されています。上記の通りこのマクロはコード生成の最終段階で呼び出されます。以降は Assertion モジュールの定義の残りです。

  defmacro test(description, do: test_block) do
    test_func = String.to_atom(description)
    quote do
      @tests {unquote(test_func), unquote(description)}
      def unquote(test_func)(), do: unquote(test_block)
    end
  end

  # {:==, [context: Elixir, import: Kernel], [5, 5]}
  defmacro assert({operator, _, [lhs, rhs]}) do
    quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do
      Assertion.Test.assert(operator, lhs, rhs)
    end
  end
end

test と assert が定義されています。例えば試験は以下のように記述されます (一部のみ)。

defmodule MathTest do
  use Assertion
  test "integers can be added and subtracted" do
    assert 2 + 3 == 5
    assert 5 - 5 == 10
  end

module を use した後で使われています。こうして見てみるに test という macro の理解が微妙。テキストを再度確認してみます。quote do な block の中で

  • @test attribute に {unquote(test_func), unquote(description)} を蓄積 (accumulate)
  • description な名前の test_block を実行する手続きを定義

ということなのか。ここ、見事にスルーしてました。かなりキモの部分スね。というか次の assert もなかなかアレゲです。

  • bind_quoted については (3) なエントリで確認してました
  • マクロの中で複数回 quote するようなケースではこうしておいた方が良い
  • assert の中身は Assertion.Test.assert の呼び出し

ということで、次は Assertion.Test module です。こちらが本体、になるのかどうか。まずは run からということで以下。

defmodule Assertion.Test do
  def run(tests, module) do
    Enum.each tests, fn {test_func, description} ->
      case apply(module, test_func, []) do
        :ok             -> IO.write "."
        {:fail, reason} -> IO.puts """
        ======================================
        FAILURE: #{description}
        ======================================
        #{reason}
        """
      end
    end
  end

Assertion は Assertion.Test の wrapper なんですね。今更ですが。Assertion.run の定義を再掲します。

      def run do
        Assertion.Test.run(@tests, __MODULE__)
      end

この手続きは __before_compile__ なマクロが呼び出された時点で評価されるので、@tests への accumulate が全て終わってる、ということですね。で、apply(module, test_func, []) で test_func の値が名前の手続きが実行されるのか。

ありゃ?

この記述もスルーしてました。

    Enum.each tests, fn {test_func, description} ->
      case apply(module, test_func, []) do

これ、確かに tests は {testfunc, description} だけどいいのか的アレがナニですね。あと、Enum.each/2 のドキュメントが以下です。

assert

ここはメタでも何でもなくて、なカンジです。

  def assert(:==, lhs, rhs) when lhs == rhs do
    :ok
  end
  def assert(:==, lhs, rhs) do
    {:fail, """
    Expected:       #{lhs}
    to be equal to: #{rhs}
    """
    }
  end

ということで次は Chapter 3 になります。メタではない方もすすめたいのですが、こっちが面白くて (ぇ

Metaprogramming Elixir (5) Metaprogramming Elixir (7)

comments powered by Disqus