/var/log/messages

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

Metaprogramming Elixir (5)

コードの生成に module attributes を使う、とのこと。翻訳時にモジュールに格納されてるデータにアクセスできる、なのかどうか。

accumulate:true

というオプションが attribute を登録する時に云々、とあります。

test macro

二つの argument を受け取る、とのこと。

  • string description (キーワードリスト?)
  • do/end なコードブロック

コード例示

以下で良いのかどうか。

defmodule Assertion do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
      Module.register_attribute __MODULE__, :tests, accumulate: true
      def run do
        IO.puts "Running the tests (#{inspect @tests})"
      end
    end
  end

  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
end

これ

  • Module.register_attribute で tests という attribute を登録
  • @test という attribute を inspect

しているとのこと。次に test という macro を定義してるのですがこれは一体何なのかと。ちょっと整理してみます。

  • use Assertion__using__ なマクロが展開
  • tests て attribute が登録される
  • tests を inspect して出力する run という手続きが定義される
  • test というマクロは展開されたら caller’s context で動作?
  • @tests に test_func (description を atom にしたもの) と description の参照を設定?
  • def unquote(test_func)(), do: unquote(test_block) の意味をわかりかねています

たぶんこれはまだ足りてないはず。

あら?

なんかおかしいと思ったら assert_step2.exs に追加なのか。以下を assertion.exs としてでっちあげて

defmodule Assertion do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
      Module.register_attribute __MODULE__, :tests, accumulate: true
      def run do
        IO.puts "Running the tests (#{inspect @tests})"
      end
    end
  end

  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

defmodule Assertion.Test do
  def assert(:==, lhs, rhs) when lhs == rhs do
    IO.write "."
  end
  def assert(:==, lhs, rhs) do
    IO.puts """
    FAILURE:
    Expected:      #{lhs}
    to be equal to #{rhs}
    """
  end
  def assert(:>, lhs, rhs) when lhs > rhs do
    IO.write "."
  end
  def assert(:>, lhs, rhs) do
    IO.puts"""
    FAILURE:
    Expected:           #{lhs}
    to be greater than: #{rhs} 
    """
  end
end

なんとなく同じ状態になったのかどうか。

iex(1)> c "assertion.exs"
[Assertion.Test, Assertion]
iex(2)> c "math_test_step1.exs"
[MathTest]
iex(3)> MathTest.__info__(:functions)
["integers can be added and subtracted": 0, run: 0]
iex(4)> MathTest.run
Running the tests ([])
:ok

で、これは何が起きたのかというと

  • @tests attribute が空
  • Assertion の using なブロックが微妙らしい

何が問題なのか、というと

  • run/0 は tests 属性を登録した直後に定義
  • run の定義は use Assertion の宣言で MathTest モジュール内で展開される
  • この時点で @tests は空
  • test が eval される時点まで展開を遅らせたい

というこで

  • before_compile fook が用意されている

とのこと。

compile-time hooks

@before_compile という特別な module attribute でコンパイルが終了する直前に追加の手順が必要であることをコンパイラに通知する、とのこと。また、@before_compile属性は、__before_compile __ / 1 マクロを定義する必要があるモジュール引数を受け入れる、とあります。これが追加の手順になるのだな。ちょっとテキストと微妙に差異があるのですが、before_compile.exs として以下を投入。

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
  defmacro __before_compile__(_env) do
    quote do
      def run do
        IO.puts "Running the tests (#{inspect @tests})"
      end
    end
  end

  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

で、iex で以下。

iex(3)> c "before_compile.exs" 
warning: redefining module Assertion (current version defined in memory)
  before_compile.exs:1

[Assertion]
iex(4)> c "math_test_step1.exs"
[MathTest]
iex(5)> MathTest.run
Running the tests (["integers can be added and subtracted": "integers can be added and subtracted"])
:ok

またテストは動いていませんが @test の用意が終わった後に run という手続きが定義されたことがわかります。これを踏まえて assertion.exs を改良します。これが test framework になるのか。

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
  defmacro __before_compile__(_env) do
    quote do
      def run do
        Assertion.Test.run(@tests, __MODULE__)
      end
    end
  end

  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

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
  def assert(:==, lhs, rhs) when lhs == rhs do
    :ok
  end
  def assert(:==, lhs, rhs) do
    {:fail, """
    Expected:       #{lhs}
    to be equal to: #{rhs}
    """
    }
  end
  def assert(:>, lhs, rhs) when lhs > rhs do
    :ok
  end
  def assert(:>, lhs, rhs) do
    {:fail, """
    Expected:       #{lhs}
    to be equal to: #{rhs}
    """
    }
  end
end

run の定義を直すのを忘れていたのはご愛嬌。で、試験は以下です。

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

コンパイルして実行、が以下。

iex(1)> c "assertion.exs"
[Assertion.Test, Assertion]
iex(2)> c "math_test_final.exs"
[MathTest]
iex(3)> MathTest.run
.======================================
FAILURE: integers can be added and subtracted
======================================
Expected:       0
to be equal to: 10


:ok

次回エントリにて asertion.exs の最終形をべろべろしつつ復習の方向です。

Metaprogramming Elixir (4) Metaprogramming Elixir (6)

comments powered by Disqus