LoginSignup
19
3

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-05

この記事は 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になる」です.お楽しみに!

19
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
3