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

# TL;DR

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

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

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

※このネタはだいぶ前に下書きしてたのを先日の Elixir Fest の配信で「[黒魔術](https://speakerdeck.com/tamanugi/elixir-hei-mo-shu-ru-men)」という単語を見て思い出して仕上げたものです。

# はじめに

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

```elixir
# 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 グループ](https://groups.google.com/d/msg/elixir-lang-core/WCzeA9KpIMM/5P37UjAfbQQJ)

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

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

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

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

# Sigil

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

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

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

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

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

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

[Kernel モジュール](https://github.com/elixir-lang/elixir/blob/776225f4e1119fb052ea0b013ba0d61a7a093719/lib/elixir/lib/kernel.ex#L4996) からの抜粋が以下です（他にも guard 節違いの定義がいくつかありますが省略します）：


```elixir
  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
```

この関数（マクロ）の引数、

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

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

[公式ガイドの説明](http://elixir-lang.org/getting-started/sigils.html#custom-sigils)によると、

```elixir
~r/foo/i
```

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

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

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

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

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

```elixir
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 型にコンパイルして（２行目）、 `Macro.escape` で AST 形式に変換（3行目）しています。

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

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

```elixir: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
```

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

```elixir
** (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](https://hexdocs.pm/elixir/Code.html#string_to_quoted!/2) があったのでこれを使ってみます。

```elixir: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
```

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

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

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

# まとめ

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

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