/var/log/messages

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

Metaprogramming Elixir (3)

Chapter 2 に突入。とりあえず if な特殊形式な macro を作る模様。

defmodule ControlFlow do
  defmacro my_if(expr, do: if_block), do: if(expr, do: if_block, else: nil)
  defmacro my_if(expr, do: if_block, else: else_block) do
    quote do
      case unquote(expr) do
        result when result in [false, nil] -> unquote(else_block)
        _ -> unquote(if_block)
      end
    end
  end
end

なんとなくこれは if な macro の定義そのまんまなのではと思うんですがどうなのか。

iex(6)> c "if_recreated.exs"
[ControlFlow]
iex(7)> require ControlFlow
ControlFlow
iex(8)> ControlFlow.my_if 1 == 1 do
...(8)> "correct"
...(8)> else
...(8)> "incorrect"
...(8)> end
"correct"

While の追加

追加したら以下なコードが実装できるとのこと。

while Process.alive?(pid) do
  send pid, {self, :ping}
  receive do
    {^pid, :pong -> IO.puts "Got pong"}
  after 2000 -> break
  end
end

何だろう infinite stream 使って云々とか書いてあるぞ。例示されてる実装を盛り込んでみます。

defmodule Loop do
  defmacro while(expression, do: block) do
    quote do
      for _ <- Stream.cycle([:ok]) do
        if unquote(expression) do
          unquote(block)
        else
          # break out of loop
        end
      end
    end
  end
end

なるほど、こう書くのか。というかこのままでは条件が false になったときにループは脱出できないですね。このあたりは後々実装になるはず。実行例出てますが C-c で中断させていますね。(引用略)

次のステップ

とりあえずループさせることはできるようになっています。ループからの脱出は try/catch を使う? という記述がありますね。例示されてる実装を確認してみます。

defmodule Loop do
  defmacro while(expression, do: block) do
    quote do
      try do
        for _ <- Stream.cycle([:ok]) do
          if unquote(expression) do
            unquote(block)
          else
            throw :break
          end
        end
      catch
        :break -> :ok
      end
    end
  end
end

try なブロックの中で for なループが動いて条件式が false のときに :break を throw しています。これで完成? って思ったらもう少し実装追加しています。else なブロックで直接 :break を throw するのは危険、ということなんすかね。

defmodule Loop do
  defmacro while(expression, do: block) do
    quote do
      try do
        for _ <- Stream.cycle([:ok]) do
          if unquote(expression) do
            unquote(block)
          else
            throw :break
          end
        end
      catch
        Loop.break
      end
    end
  end
  def break, do: throw :break
end

なるほど。

テスツ

ちっさいテスティングフレームワークを、とのこと。Elixir のは ExUnit って名前のフレームワークなのですね。以下なナニが例示されています。

defmodule Test do
  import Assertion
  def run
    assert 5 == 5
    assert 2 > 0
    assert 10 < 1
  end
end

これ、repl 上で云々してみます。

Assertion が無い

なるほど、これを今から作るのね。上の例だと assert の引数は boolean なナニを戻す演算子と左辺値と右辺値になっていて

iex(10)> quote do: 5 == 5
{:==, [context: Elixir, import: Kernel], [5, 5]}

AST になってればそれぞれがさくっと取り出せる、ということなのかどうか。

defmodule Assertion do
  # {:==, [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

これがプロトタイプなのか。たしかに bind_quoted なるソレは初見かもしれません。

bind_quoted

quote のオプションらしい。以下な記載が等価、とあります。

    quote bind_quoted: [operator: operator, lhs: lhs, rhs: rhs] do
      Assertion.Test.assert(operator, lhs, rhs)
    end

もう一方が以下。

    quote do
      Assertion.Test.assert(unquote (operator), unquote(lhs), unquote(rhs))
    end

これの何が嬉しいのか、ということを理解するために Debugger な実装を書いてみれ、とのこと。

defmodule Debugger do
  defmacro log(expression) do
    if Application.get_env(:debugger, :log_level) == :debug do
      quote do
        IO.puts "======================="
        IO.inspect unquote(expression)
        IO.puts "======================="
        unquote(expression)
      end
    else
      expression
    end
  end
end

iex での実行例な記載もあります。

iex(11)> c "debugger.exs"
[Debugger]
iex(12)> require Debugger
Debugger
iex(13)> Application.put_env(:debugger, :log_level, :debug)
:ok
iex(14)> remote_api_call = fn -> IO.puts("calling remote API...")
...(14)> end
#Function<20.128620087/0 in :erl_eval.expr/5>
iex(15)> Debugger.log(remote_api_call.())
=======================
calling remote API...
:ok
=======================
calling remote API...
:ok

二度、呼びだされていますね。たしかに二度 unquote してるので、なのか。これを bind_quoted を使って解決、とありますね。

defmodule Debugger do
  defmacro log(expression) do
    if Application.get_env(:debugger, :log_level) == :debug do
      quote bind_quoted: [expression: expression] do
        IO.puts "======================="
        IO.inspect expression
        IO.puts "======================="
        expression
      end
    else
      expression
    end
  end
end

テストしてみました。

iex(16)> c "debugger_fixed.exs"
warning: redefining module Debugger (current version defined in memory)
  debugger_fixed.exs:1

[Debugger]
iex(17)> Debugger.log(remote_api_call.())
calling remote API...
=======================
:ok
=======================
:ok

なるほど。こりゃいいすね。

VM のパターンマッチングの活用

ここ、面白そうなのでゆっくりめに確認すすめます。

defmodule Assertion do
  # {:==, [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

パタンマッチ使うってこゆことだったのか。これができるから Elixir いいすよね。

iex(20)> assert 1 > 2
FAILURE:
Expected:           1
to be greater than: 2 

:ok
iex(21)> assert 1 == 2
FAILURE:
Expected:      1
to be equal to 2

:ok
iex(22)> assert 2 > 1
.:ok
iex(23)> assert 2 == 2
.:ok

そしてこんなことができるのか。

defmodule MathTest do
  import Assertion
  def run do
    assert 5 == 5
    assert 10 > 0
    assert 1 > 2
    assert 10 * 10 == 100
  end
end

上のは先に例示されていたテスツ実装ですね。

iex(24)> c "math_test_import.exs"
[MathTest]
iex(25)> MathTest.run
..FAILURE:
Expected:           1
to be greater than: 2 

.:ok
iex(26)> 

一度ここで手を止めます。面白すぎてキリが無い。

Metaprogramming Elixir (2) fukuoka.ex#19

comments powered by Disqus