この記事は fukuoka.ex Elixir/Phoenix Advent Calendar 2020 の5日目です.
4日目は,@takasehideki 先生の「ElixirでIoT#4.1.2:Docker(とVS Code)だけ!でNerves開発環境を整備する」でした!
動作環境
- macOS Mojave(10.14.6)
- Erlang/OTP 22 [erts-10.5] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:1] [hipe] [sharing-preserving]
- Elixir 1.10.4 (compiled with Erlang/OTP 21)
必要知識
匿名関数
ElixirではEnum.mapやReduceに関数を渡すことでデータを加工することができます.
標準で提供されているモジュールの関数を処理として渡すというのが主な使い時です.
下記にIExで定義,呼び出しするコードを載せます.
iex> func = fn x -> x + 1 end
#Function<7.126501267/1 in :erl_eval.expr/5>
iex> func.(3)
4
では,上記の変数func
が何者か知りたいのでi
をつけて確認します.
iex> i func
Term
#Function<7.126501267/1 in :erl_eval.expr/5>
Data type
Function
Type
local
Arity
1
Description
This is an anonymous function.
Implemented protocols
Enumerable, IEx.Info, Inspect
匿名関数を引数にする
匿名関数を引数にすれば,渡した先で関数が実行できます.
iex> func_exec = fn data, func -> func.(data) end
#Function<13.126501267/2 in :erl_eval.expr/5>
iex> func_exec.(1, fn x -> x + 1 end)
2
関数の仮引数でパターンマッチ
匿名関数の仮引数でもパターンマッチができます.
iex> func_ptn = fn {x, y} -> x + y end
#Function<7.126501267/1 in :erl_eval.expr/5>
iex> func_ptn.({1, 2})
3
匿名関数をパターンマッチ?
基本を押さえたところで本題です.見やすさのために名前付き関数で説明します.
今回やることのイメージは,
任意の匿名関数を関数のパターンマッチで分けて処理を変えることです.
下記は,値xの2乗が来たとき,計算を:math.pow/2
で行うように書き換えるコードのイメージです.
# ※イメージです
defmodule Sample do
def replace_to_pow(fn x -> x * x end) do
:math.pow(x, 2)
end
end
こんなヘンテコな文法は通りませんので,メタプログラミングでなんとかしましょう.
$ elixir sample.exs
invalid pattern in match, fn is not allowed in matches
匿名関数のAST
匿名関数をquote
で包んでおくと,コンパイラに渡される中間表現を取得できます.
今後の説明では,コードの中間表現をAST
と表記します.
後の手順でコピペできるように一工夫します.
iex> ( quote do: fn x -> x * x end ) |> Macro.escape() |> Macro.to_string() |> IO.puts
{:fn, [], [{:->, [], [[{:x, [], Elixir}], {:*, [context: Elixir, import: Kernel], [{:x, [], Elixir}, {:x, [], Elixir}]}]}]}
AST |> 仮引数
ではreplace
関数の引数に上記の結果をコピペします.
defmodule Sample do
def replace_to_pow({:fn, [], [{:->, [], [[{:x, [], Elixir}], {:*, [context: Elixir, import: Kernel], [{:x, [], Elixir}, {:x, [], Elixir}]}]}]}) do
IO.puts("MATCH!!")
end
end
(quote do: fn x -> x * x end)
|> Sample.replace_to_pow()
|> Macro.to_string()
|> IO.puts()
コンパイルが通るか確認しましょう.
$ elixir sample.exs
MATCH!!
:ok
匿名関数をパターンマッチ!
では,最後に出力を整えておしまいです.
本当は仮引数内部の変数x
をちゃんと解析しないといけないのですが,難易度が跳ね上がるので本記事では取り扱いません.
defmodule Sample do
def replace_to_pow({:fn, [], [{:->, [], [[{:x, [], Elixir}], {:*, [context: Elixir, import: Kernel], [{:x, [], Elixir}, {:x, [], Elixir}]}]}]}) do
quote do: fn x -> :math.pow(x, 2) end
end
end
(quote do: fn x -> x * x end)
|> Sample.replace_to_pow()
|> Macro.to_string()
|> IO.puts()
実行結果です.
$ elixir escape.exs
BEFORE
fn x -> x * x end
AFTER
fn x -> :math.pow(x, 2) end
まとめ
実はASTを仮引数にしてもコンパイルは通る.
AST・コードの中間表現とカッコよくいってますが,実際はタプルやリスト,基本型が再帰的に組み合わさっているだけです.
次記事の予定・あとがき
メタプロは本来,コードの自動生成に役立つものです.
なので,次の記事は紹介したネタを元にコードの自動置換に触れていく予定です.
自動置換が難しかったので,本記事を入門編的な位置付けにしました.
最後に
※マクロは用法用量を守って正しくお使いください.
fukuoka.ex Elixir/Phoenix Advent Calendar 2020 の6日目は @yoshitia さんの「やがてelixirになる」です.お楽しみに!