スポンサーリンク

2015年9月3日

[Elixir]マクロで特定の値にマッチする関数を展開する (後、assertマクロの手法を少し)

とある錬金術師の万能薬(Elixir)

Goal

マクロで特定の値にマッチする関数を展開する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.5

Wait a minute

マクロを使って特定の値にしかマッチしない関数を展開します。
覚えた技法の復習がてら作っているので、
一例として見て頂ければと思います。

Index

Expand the matching function
|> Matching function
|> First step
|> Second step
|> Extra

Matching function

関数の引数が特定の値にマッチしていないと、
関数にマッチしないようにしたい。

Example:

defmodule SampleCode do
  def match_test(1, 2) do
    true
  end
end

Result:

iex> SampleCode.match_test(1, 2)
true

Add Result:

実際に、マッチしない値を指定して関数を呼び出してみると…
関数にマッチしないとエラーを出してくれる。
iex> SampleCode.match_test(2, 2)
** (FunctionClauseError) no function clause matching in SampleCode.match_test/2
    (sample_code) lib/sample_code.ex:2: SampleCode.match_test(2, 2)
iex> SampleCode.match_test(2, 1)
** (FunctionClauseError) no function clause matching in SampleCode.match_test/2
    (sample_code) lib/sample_code.ex:2: SampleCode.match_test(2, 1)
こんな感じで、引数の値にマッチしないと関数にマッチしないようにできる。
これをマクロで応用して、少し遊んでみましょう。

First step

第一段階のソースコード。
マクロを呼び出すと、その引数値にしかマッチしない関数を展開したい。
そして、展開した関数を特定の値をキーにして実行するようにする。

Example:

defmodule MatchMacroSample do
  defmacro set(key, do_clause) do
    run = Keyword.get(do_clause, :do, nil)

    quote do
      defp get(unquote(key)) do
        unquote(run)
      end
    end
  end
end

defmodule UseMatchMacroSample do
  import MatchMacroSample

  set "key1", do: "value1"
  set "key2", do: "value2"

  def get_key_match(key) do
    get(key)
  end
end

Result:

iex> UseMatchMacroSample.get_key_match("key1")
"value1"
iex> UseMatchMacroSample.get_key_match("key2")
"value2"
iex> UseMatchMacroSample.get_key_match("key3")
** (FunctionClauseError) no function clause matching in UseMatchMacroSample.get/1
    (macro_sample) lib/macro_sample.ex:26: UseMatchMacroSample.get("key3")

Second step

第二段階のソースコード。
動作は変わらないようにして、
呼び出す関数をuseで展開するようにする。
defmodule MatchMacroSample do
  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)

      def get_key_match(key) do
        get(key)
      end
    end
  end

  defmacro set(key, do_clause) do
    run = Keyword.get(do_clause, :do, nil)

    quote do
      defp get(unquote(key)) do
        unquote(run)
      end
    end
  end
end

defmodule UseMatchMacroSample do
  use MatchMacroSample

  set "key1", do: "value1"
  set "key2", do: "value2"
end

Result:

iex> UseMatchMacroSample.get_key_match("key1")
"value1"
iex> UseMatchMacroSample.get_key_match("key2")
"value2"
iex> UseMatchMacroSample.get_key_match("key3")
** (FunctionClauseError) no function clause matching in UseMatchMacroSample.get/1
    (macro_sample) lib/macro_sample.ex:26: UseMatchMacroSample.get("key3")
はい完成~

Extra

assertマクロを読んだメモ。

引用元: elixir-lang/elixir - assertions.ex#L66

defmacro assert({:=, _, [left, right]} = assertion) do
上記のような定義をしていた。
何これと思って、assertに渡す引数部分をquoteしてみた。

Try:

iex> quote do: 1 == 2
{:==, [context: Elixir, import: Kernel], [1, 2]}
iex> quote do: 1 != 2
{:!=, [context: Elixir, import: Kernel], [1, 2]}
iex> quote do: 1 > 2
{:>, [context: Elixir, import: Kernel], [1, 2]}
iex> quote do: 1 < 2
{:<, [context: Elixir, import: Kernel], [1, 2]}
おk、分かった。
assertの引数部分をAST(タプル)にマッチさせているわけだ。
またassertions.exの中に気になる記述があったのでこれも試す。
こんな感じの記述。

Example:

unquote(operator)(left, right)

Try:

iex> quote do: unquote(:==)(1, 2)
{:==, [], [1, 2]}
iex> quoted = quote do: unquote(:==)(1, 2)
{:==, [], [1, 2]}
iex> Macro.to_string(quoted)
"1 == 2"
Σ(゚Д゚) (こんな心境だった。
それはともかく、マクロ内だとこんな書き方ができるわけですね・・・
ちなみに、普通に記述しても動かせなかった。
適当な例…

Try:

iex> == 1, 2
** (SyntaxError) iex:10: syntax error before: '=='

iex> ==(1, 2)
** (SyntaxError) iex:10: syntax error before: '=='

iex> :== 1, 2
** (SyntaxError) iex:10: syntax error before: 1

iex> :==(1, 2)
** (SyntaxError) iex:10: syntax error before: '('

iex> (==)(1, 2)
** (SyntaxError) iex:10: syntax error before: '=='

iex> (==).(1, 2)
** (SyntaxError) iex:10: syntax error before: '=='
ASTにしているからこそできる方法ですね。
マクロをコンパイルする順番の話に発展するので、ここまで。

Speaking to oneself

コード自体に意味はあまりありません。
最初に書いた通り、ただのサンプルですので。
何に使えるのかと言ったところですが…
Plugやルーティングなどで使ってる技法のようです。
どのソースコードかまでは知らない(笑)
そろそろ、次のレベルに行って打ちのめされてきても良さそうな気がしてきた。
少し難しめのマクロでも解読してみましょうかね…
assertマクロですが、驚きましたね。
引数にタプル(AST?)を使ってマッチングしていたり、マクロ特有(?)の書き方をしていたり、
あんな方法もあるんだなっと勉強になりました。

Bibliography

人気の投稿