[Elixir]匿名関数をパターンマッチする


この記事は 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で行うように書き換えるコードのイメージです.

sample.exs
# ※イメージです
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関数の引数に上記の結果をコピペします.

sample.exs
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をちゃんと解析しないといけないのですが,難易度が跳ね上がるので本記事では取り扱いません.

sample.exs
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になる」です.お楽しみに!