LoginSignup
4
0

More than 3 years have passed since last update.

Elixir で黒魔術(マクロ & sigil)を使って無名関数を作れるようにしてみる

Last updated at Posted at 2019-06-19

TL;DR

Elixir のマクロを利用して、以下のように~f(...) で無名関数が作れるようにしてみます。

iex> Enum.reduce 1..100, 0, ~f(x, y -> x + y)
# -> 5050

また、その過程で Kernel モジュールにおける正規表現 sigil の実装を調べて、マクロと sigil の組み合わせ方を見てみます。

※このネタはだいぶ前に下書きしてたのを先日の Elixir Fest の配信で「黒魔術」という単語を見て思い出して仕上げたものです。

はじめに

Elixir を触ってみて、Ruby 風シンタックスにパターンマッチや pipe オペレータなどのおかげで非常に簡潔に書けるのが気に入っているのですが、ちょっと気になったのが無名関数を定義するときの構文です。
Elixir では、以下の2通りの無名関数定義の構文があります:

# 1. fn ... end
Enum.reduce 1..100, 0, fn x, y -> x + y end

# 2. capture syntax
Enum.reduce 1..100, 0, &(&1 + &2)

後者の capture syntax (捕捉構文) は、上記のように処理の内容が非常に単純な場合は簡潔に書けて良いのですが、多少複雑になってくると、変数名で意図を表現できないためどんどん読みづらくなってきます。

また、前者のfn ... endは、複数行で書くときは特に気にならないのですが、1行で書くときに開始と終了の対応がぱっと見で分かりづらい気がします(個人の感想です)。Ruby のブロックとか JS のアロー構文のように{ ... }みたいな形で書きたいなという気がします(個人の感想です)。

この、特にインラインで書いたときのendを美しくないと思う人はそれなりにいるようで、過去のMLで議論がありました。
syntax sugar for fn() ? - Google グループ

その中で、Elixir 作者の José Valim さんがこれならいけるでと、( ... )を使った構文を提案しています:

Enum.reduce list, 0, (x, y -> x + y)

これはいいぜと思ったものの、この議論が2013年の末で、その後数年経った現在も普通の無名関数定義には使える様になっていないようです。

ですが、Elixirの持ってる機能(マクロとsigil)だけで似たことが実現できるんじゃないかと思ったので試してみました。

Sigil

sigil(シジル)、調べてみると「主に西洋魔術で使われる図形、記号、紋章、線形」というえらい中二心をくすぐる説明があります。全然馴染みない言葉だったんですが、最近見てる Game of Thrones では「旗印」として出てきてました。

Elixirにおいては、チルダ~と英字1文字から始まるものを sigil と呼んでいて、以下のような各種リテラルもこの機能を利用して実現しています:

# 正規表現リテラル
iex> ~r/foo/i
# -> ~r/foo/i

# 単語リスト リテラル
iex> ~w(foo bar bat)
# -> ["foo", "bar", "bat"]

また、Elixir の sigil はユーザー定義も可能で、ここらへんの話は公式ガイドの Sigilsにまとまっています。
ここの説明の最後に、マクロと組み合わせて使う場合は Kernel モジュールでの sigil_* の実装を見てみるといいよとあったので見てみましょう。

Kernel モジュールにおける 正規表現 sigil の実装

Kernel モジュール からの抜粋が以下です(他にも guard 節違いの定義がいくつかありますが省略します):

  defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do
    binary = Macro.unescape_string(string, fn(x) -> Regex.unescape_map(x) end)
    regex  = Regex.compile!(binary, :binary.list_to_bin(options))
    Macro.escape(regex)
  end

この関数(マクロ)の引数、

{:<<>>, _meta, [string]}, options

これ、何なんでしょう。黒魔術感があります。どうにかなんとなく理解したのでちょっと解説してみます。

公式ガイドの説明によると、

~r/foo/i

という正規表現リテラルは、

sigil_r(<<"foo">>, 'i')

の呼び出しと等しい、とあります。

sigil_* が関数として定義されている場合、引数はそのまま<<"foo">>, 'i'として渡されますが、sigil_rは上記の通りdefmacroによってマクロとして定義されており、引数は AST(抽象構文木)に変換された形(quoted form) で渡されます。

上記の引数<<"foo">>'i'を quote するとどうなるかというと、

iex> quote do: <<"foo">>
# -> {:<<>>, [], ["foo"]}

iex> quote do: 'i'
# -> 'i'

はい、上記のsigil_rマクロの引数と似た形になりました。2つ目は charのリストなので、quoteしてもそのままです。
そんなわけで、~r/foo/iと書くと、上記のsigil_rマクロが、string="foo", options='i' という引数と共に呼ばれることになります。

sigil_r マクロの中では、受け取った string のエスケープされた文字を元に戻して(1行目)、Regex.compile! で Regex 型にコンパイルして(2行目)、 Macro.escape で AST 形式に変換(3行目)しています。

無名関数を返す sigil を定義する

さて、sigil_r の定義を参考にして、 ~f() で無名関数を返すマクロを書いてみましょう。
単純に考えると、 string から Regex を作る代わりに無名関数を作ればいいだけです。Code.eval_string が使えそうです。というわけで以下のようにしました。

func_sigil(notwork).exs
defmodule FuncSigil do
  defmacro sigil_f({:<<>>, meta, [string]}, _options) when is_binary(string) do
    bin = Macro.unescape_string "(fn #{string} end)"
    func = Code.eval_string bin
    Macro.escape(func)
  end
end

ところが、これを呼び出してみると以下のようなエラーが出ます。

** (ArgumentError) cannot escape #Function<12.52032458/2 in :erl_eval.expr/5>. The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, pids and remote functions in the format &Mod.fun/arity

無名関数は Macro.escape/1 で扱えないようです。困って調べたところ、string から直接 quoted form を返すといううってつけの関数 Code.string_to_quoted があったのでこれを使ってみます。

func_sigil.exs
defmodule FuncSigil do
  defmacro sigil_f({:<<>>, _meta, [string]}, _options) when is_binary(string) do
    bin = Macro.unescape_string("(fn #{string} end)")
    Code.string_to_quoted!(bin)
  end
end

ではこれで試してみましょう

iex> import FuncSigil
# -> FuncSigil
iex> Enum.reduce 1..100, 0, ~f(x, y -> x + y)
# -> 5050

やったぜ!ちゃんと関数が動いてるようです。

まとめ

Elixir のマクロと sigil を利用して無名関数を作れるようになりました。
まあこれで関数を書いても(sigil の内容は string として扱われるので)エディタのハイライトや補完も効かないですし、複数 clause の関数が定義できなかったり、全然実用的ではありません。
ですが、 Elixir の黒魔術が強力で、しかも意外と簡単に使えたよ、ということを示す例としてはわりと面白いのではないでしょうか。どうでしょう。

書いてはみたもののよく理解できてない部分も多いのでツッコミ大歓迎です。
それではまた。

4
0
0

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
4
0