テストフレームワークと書きながら、そこは余り多く書いていませんが…
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はその範囲内では無効に設定されます。
なお、このほかにも
にある、
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/master/lib/plug/mime.types
のリストからmimeのtypeとextensionsを得て、atomにしていくというコンパイル時のビルド処理。
なるほど。。。
思ったよりも力技でした。。。
Compile-time code generationの例
| defmodule Translator do | |
| defmacro __using__(_options) do | |
| quote do | |
| Module.register_attribute __MODULE__, :locales, accumulate: true, persist: false | |
| import unquote(__MODULE__), only: [locale: 2] | |
| @before_compile unquote(__MODULE__) | |
| end | |
| end | |
| defmacro __before_compile__(env) do | |
| compile(Module.get_attribute(env.module, :locales)) | |
| end | |
| defmacro locale(name, mappings) do | |
| quote bind_quoted: [name: name, mappings: mappings] do | |
| @locales {name, mappings} | |
| end | |
| end | |
| def compile(translations) do | |
| translations_ast = for {locale, source} <- translations do | |
| deftranslations(locale, "", source) | |
| end | |
| quote do | |
| def t(locale, path, bindings \\ []) | |
| unquote(translations_ast) | |
| def t(_locale, _path, _bindings), do: {:error, :no_translation} | |
| end | |
| end | |
| defp deftranslations(locales, current_path, translations) do | |
| for {key, val} <- translations do | |
| path = append_path(current_path, key) | |
| if Keyword.keyword?(val) do | |
| deftranslations(locales, path, val) | |
| else | |
| quote do | |
| def t(unquote(locales), unquote(path), bindings) do | |
| unquote(interpolate(val)) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| defp interpolate(string) do | |
| IO.inspect string | |
| ~r/(?<head>)%{[^}]+}(?<tail>)/ | |
| |> Regex.split(string, on: [:head, :tail]) | |
| |> IO.inspect | |
| |> Enum.reduce "", fn | |
| <<"%{" <> rest>>, acc -> | |
| key = String.to_atom(String.rstrip(rest, ?})) | |
| quote do | |
| unquote(acc) <> to_string(Dict.fetch!(bindings, unquote(key))) | |
| end | |
| segment, acc -> quote do: (unquote(acc) <> unquote(segment)) | |
| end | |
| end | |
| defp append_path("", next), do: to_string(next) | |
| defp append_path(current, next), do: "#{current}.#{next}" | |
| end | |
| defmodule I18n do | |
| use Translator | |
| locale "en", | |
| flash: [ | |
| hello: "Hello %{first} %{last}", | |
| bye: "Bye, %{name}" | |
| ], | |
| users: [ | |
| title: "Users", | |
| ] | |
| locale "ja", | |
| flash: [ | |
| hello: "こんにちは %{first} %{last}", | |
| bye: "ばいばい, %{name}" | |
| ], | |
| users: [ | |
| title: "ユーザ", | |
| ] | |
| end | |
| defmodule Sample do | |
| import I18n | |
| def run do | |
| IO.inspect I18n.t("en", "flash.hello", first: "Chris", last: "McCord") | |
| IO.inspect I18n.t("ja", "flash.hello", first: "Chris", last: "McCord") | |
| IO.inspect I18n.t("en", "users.title") | |
| IO.inspect I18n.t("ja", "users.title") | |
| end | |
| end |
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、読む分にはだいぶん読める量が増えてきた気がする。