[Elixir]Macroで簡単なテストフレームワークを作ってみる

テストフレームワークと書きながら、そこは余り多く書いていませんが…

Elixirは、

Elixir is small because it doesn’t have ti include all common features.

とのこと。なので、他に必要な拡張が欲しければ、自分で作ってねという言語らしい。基本は小さく、という立ち方ですね。

bind_quoted

通常、 quote で囲まれたところのうち、変数をそのまま使いたい場合は unquote します。ただ、都度 unquote するのは少し面倒だし、場合によっては意図しない動作になります。その問題を解決するために、 bind_quote があります。

例えば、以下のASTが bind_quote の有無でどう変わるのかを覗いてみます。

defmodule Debugger do
  defmacro log(expression) do
    if Application.get_env(:debugger, :log_level) == :debug do
      quote bind_quoted: [expression: expression] do # or quote do
        IO.puts "====="
        IO.puts expression # or IO.puts unquote expression
        IO.puts "====="
      end
      |> IO.inspect
    else
      expression
    end
  end
end
  • unquoteのみ
{:__block__, [],
 [{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["====="]},
  {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],
   [{{:., [line: 24], [{:re, [line: 24], nil}]}, [line: 24], []}]},
  {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["====="]}]}
  • bindされたとき
{:__block__, [],
 [{:=, [],
   [{:expression, [], Debugger},
    {{:., [line: 24], [{:re, [line: 24], nil}]}, [line: 24], []}]},
  {:__block__, [],
   [{{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["====="]},
    {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [],
     [{:expression, [], Debugger}]},
    {{:., [], [{:__aliases__, [alias: false], [:IO]}, :puts]}, [], ["====="]}]}]}

bind_quoteのときは、最初に処理が関連付けられて、それを以降のquoteでaliasのようにして呼び出している、という処理をするようですね。unquoteの有無で意味が大きく異なる処理もあるので、これは良さそうです。

このbind_quoteが有効な時は、unquoteはその範囲内では無効に設定されます。

なお、このほかにも

https://github.com/elixir-lang/elixir/blob/f7183440716d705bfaabdc54c216d87cebacd9a7/lib/elixir/src/elixir_exp.erl#L146

にある、

ValidOpts = [context, location, line, file, unquote, bind_quoted],

がquoteのときにしていできる、特別なoptionのようです。

テストフレームワーク

簡単なテストフレームワークを書きました。以下リポジトリに突っ込んでいます。
内容自体は簡単で、

  • assert の実装
  • test で始まるメソッドをテストケースとして認識、実施する

というものです。

https://github.com/KazuCocoa/my_mini_ex_test_assertion/tree/master

サンプルを見ながらの実装ですが、これはそんなに難しくないです。

Compile-Time code generation

コンパイル時にコードを生成、メソッドなんかで定義されるmacroの話です。

unquoteが、quoteの外で使われている場合があります。

defmodule Fragment do
  for {name, val} <- [one: 1, two: 2, three: 3] do
    def unquote(name)(), do: unquote(val)
  end
end

これにより、以下のように動的な関数を定義することができる。

Fragment.one/0
Fragment.two/0
Fragment.three/0

ここで、Elixirは、外部ファイルに依存している箇所を明示することができる。

@external_resource mimes_path = Path.join([__DIR__, "mimes.txt"])

これにより、Elixirは mimes_path が変更されていれば、コンパイル時に再度コンパイルを走らせることができます。

少し脱線

PlugのMIME typeの処理箇所、この quote 外の unquote を使っているのですね。

https://github.com/elixir-lang/plug/blob/0118337b990aa2109a7b9152ea1e244a37c7dd07/lib/plug/mime.ex#L95

これは、

https://github.com/elixir-lang/plug/blob/master/lib/plug/mime.types

のリストからmimeのtypeとextensionsを得て、atomにしていくというコンパイル時のビルド処理。

なるほど。。。
思ったよりも力技でした。。。

Compile-time code generationの例

Macro.to_string 、良いですね。ASTを紐解いて表現してくれる。

Compile-time code generation by remote api

このコード生成、例えば、取得したAPIの結果を、動的にメソッドとして定義して実行することも可能です。

HTTPoisonと、Poisonを使って、以下のようにGitHubから取得できる名前をそのままメソッドとして利用もできるようにできます。mix.exsに適当に依存関係を記入して処理を進めると、以下を実施できます。

defmodule Hub do
  HTTPoison.start
  @username "KazuCocoa"

  "https://api.github.com/users/#{@username}/repos"
  |> HTTPoison.get!(["User-Agent": "Elixir"])
  |> Map.get(:body)
  |> IO.inspect
  |> Poison.decode!
  |> Enum.each fn repo ->
    def unquote(String.to_atom(repo["name"]))() do
      unquote(Macro.escape(repo))
    end
  end
end

↑をビルドした後に変換候補を表示させると以下の通り、repository名が関数名になって取得できます。

iex(1)> Hub.
AppReviewViewer/0
android-testing/0
appium/0
atom-light-ui/0
awesome-android-testing/0
device_manager/0
docker-appium/0
droid-monitor/0
ecto/0
elixir-gimei/0
elixir-github-api/0
elixir-tutorial/0
githubSample/0
hello_phoenix/0
...

これを実行すると、以下のようにURLを取得することも可能。

iex(1)> Hub.my_mini_ex_test_assertion
%{"statuses_url" => "https://api.github.com/repos/KazuCocoa/my_mini_ex_test_assertion/statuses/{sha}",
  "git_refs_url" => "https://api.github.com/repos/KazuCocoa/my_mini_ex_test_assertion/git/refs{/sha}",
  "issue_comment_url" => "https://api.github.com/repos/KazuCocoa/my_mini_ex_test_assertion/issues/comments{/number}",
  "watchers" => 0, "mirror_url" => nil,
  "languages_url" => "https://api.github.com/repos/KazuCocoa/my_mini_ex_test_assertion/languages",
  "stargazers_count" => 0, "forks" => 0, "default_branch" => "master",
  "comments_url" => "https://api.github.com/repos/KazuCocoa/my_mini_ex_test_assertion/comments{/number}",
  "commits_url" => "https://api.github.com/repos/KazuCocoa/my_mini_ex_test_assertion/commits{/sha}",
...

つまるとこ、動的に外部APIをそのままメソッドとして利用することができるようになる、ということですね。
ちょっと、これは使い道が大きそうな気がします…


macro、読む分にはだいぶん読める量が増えてきた気がする。

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s