この記事は Elixir Advent Calendar 2019 6日目の記事です
昨日は @sym_num さんで Elixirで並列Lispインタプリタ、コンパイラを作るお話でした!
Elixirの大好きなところを言ってみてって言われたら多すぎて困っちゃうと思いますが、その中でも パイプライン演算子 をあげる錬金術士さんは多いんじゃないでしょうか
今日はそんなみんな大好きパイプの正体を追ってみたいと思います
そもそもパイプって?
参考: https://elixirschool.com/ja/lessons/basics/pipe-operator/
|>
←こいつです
前の式の結果を次の関数呼び出しの第一引数に渡せるという便利なやつです。
foo(bar(baz(new_function(other_function()))))
# ↓
other_function() |> new_function() |> baz() |> bar() |> foo()
よくありがちな関数の結果を次の関数に渡す・・・というのをスマートにおしゃれに書くことができます
このパイプライン演算子とElixirの特徴の関数型言語というのがすごいマッチしていて、気持ちよくプログラミングすることができますね
早速パイプの実装を発見した我々だが・・・?
というわけで、 GithubにあるElixirのリポジトリを探検してみたところ、すぐに |>
を見つけることができました
defmacro left |> right do
[{h, _} | t] = Macro.unpipe({:|>, [], [left, right]})
fun = fn {x, pos}, acc ->
Macro.pipe(acc, x, pos)
end
:lists.foldl(fun, h, t)
end
シンプル!!けど難しい!!
まずは少しずつ解読をすすめていくことにします。
defmacro left |> right do
Elixirでは defmacro
を使うことで、マクロを定義して、メタプログラミングすることができるようです。
参考: https://elixirschool.com/ja/lessons/advanced/metaprogramming/
そして defmacro に渡ってくる値は構文木(AST)になるようです。
動作を確認するために適当に以下のようなコードを書いてみました
defmodule Sample do
defmacro left | right do
IO.inspect left
IO.inspect right
left
end
end
defmodule Main do
require Sample
import Sample
def main do
div(100, 5) | div(2)
end
end
Main.main
# $ elixir main.exs
# {:div, [line: 14], [100, 5]}
# {:div, [line: 14], [2]}
たしかに left
と right
には構文木が渡されていますね
興味深いのは div(2)
という普通だったら、怒られるような関数呼び出しを書いていても特に怒られずに実行できているところです。
このコードではマクロに構文木を渡しているだけで関数の評価までいっていないので怒られていないって感じでしょうか
[{h, _} | t] = Macro.unpipe({:|>, [], [left, right]})
Macro.unpipe/1
は lib/elixir/lib/macro.ex
に実装があります
def unpipe(expr) do
:lists.reverse(unpipe(expr, []))
end
defp unpipe({:|>, _, [left, right]}, acc) do
unpipe(right, unpipe(left, acc))
end
defp unpipe(other, acc) do
[{other, 0} | acc]
end
こちらもシンプルですね!!
試しに iex を作って直接実行してみました
iex> quote do: 100 |> div(5) |> div(2)
{:|>, [], [{:|>, [], [100, {:div, [], [5]}]}, {:div, [], [2]}]}
iex> Macro.unpipe(quote do: 100 |> div(5) |> div(2))
[{100, 0}, {{:div, [], [5]}, 0}, {{:div, [], [2]}, 0}]
どうやらパイプラインの構文木をシンプルなタプルのリストに変換してくれるみたいです。
タプルの2番目の0
は前の要素が何番目の引数に入るかを示しているようです。
{{:div, [], [5]}, 0}
だったらその前の要素の 100
が 0番目(第1引数) になるということでみたいですね!
fun = fn {x, pos}, acc ... :lists.foldl(fun, h, t)
ここまで来たら残りは一気に行きます!
fun = fn {x, pos}, acc ->
Macro.pipe(acc, x, pos)
end
:lists.foldl(fun, h, t)
foldl はReduceみたいなもので、配列の左側から渡された関数を適用していくみたいですね。
ここでは、unpipeして簡略されたASTに Macro.pipe/3
を適用していってます
Macro.pipe/3
を覗いたところ、今回主に実行されそうなのは以下の部分でした。
def pipe(expr, {op, line, args} = op_args, integer) when is_list(args) do
cond do
is_atom(op) and Identifier.unary_op(op) != :error ->
raise ArgumentError,
"cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <>
"the #{to_string(op)} operator can only take one argument"
is_atom(op) and Identifier.binary_op(op) != :error ->
raise ArgumentError,
"cannot pipe #{to_string(expr)} into #{to_string(op_args)}, " <>
"the #{to_string(op)} operator can only take two arguments"
true ->
{op, line, List.insert_at(args, integer, expr)}
end
end
{op, line, List.insert_at(args, integer, expr)}
はい、この部分ですね。
構文木のタプルの3番目は引数の配列になるのですが、そちらに差し込んでいますね。
なので最終的に 100 |> div(5)
の構文木は {:div, _, [100, 5]}
となります!
これは div(100, 5)
の構文木と一緒ですね
iex> quote do: div(100, 5)
{:div, [_], [100, 5]}
まとめ
というわけで、パイプライン演算子 |>
の正体はただのマクロでした!!
パイプライン演算子だけではなく、Elixirではほとんどの演算子がマクロによって定義されているみたいですね。
中身を覗いてすごい読みやすく実装されていて驚きました。
こんなコードを書いていけるようになりたいですね
明日は @takasehideki さんの「Erlang Ecosystem Foundationの紹介」です!!
楽しみですね