Elixirでコード変換してみよう

この記事は、[fukuoka.ex Elixir／Phoenix Advent Calendar 2019](https://qiita.com/advent-calendar/2019/fukuokaex) の4日目です。

昨日は@torifukukaiouさんの「[Elixirを書いていると将棋が強くなります(新しいことをはじめよう)](https://qiita.com/torifukukaiou/items/c22e6d53b43ddc25923b)」でした．

本記事では，並列化コンパイラPelemayのコード変換機能実装を通して得られた知見を共有します．

---

# 並列化コンパイラ Pelemay 
先日， PelemayというElixirの並列化コンパイラをリリースしました．

こんな使い方をします．

```mix.exs
def deps do
  [
    {:pelemay, "~> 0.0"},
  ]
end
```

`mix deps.get`

```elixir
defmodule M do
  require Pelemay
  import Pelemay

  defpelemay do
    def map_square (list) do
      list
      |> Enum.map(& &1 * &1)
    end
  end
end
```

上記の関数は与えられたリスト（数値のみで構成）の各要素を2乗する関数です．
`defpelemay do ~ end`で括った関数を並列化します．
`defpelemay`とは何か？　マクロで作られたライブラリ特有の構文であります．

Pelemay v0.0.4では下記の条件を満たす式を静的に並列化します．
(並列化と言いいましてもマルチコアではなく，シングルコア上でSIMD(ベクトル演算)命令を出力するように裏で色々やります．)

- Enum.mapに匿名関数を渡す
- 匿名関数の処理内部が四則演算 +, -, *, / と剰余演算である関数remのみで構成されている
- 匿名関数外の変数を**使っていない**

という具合に~~制約が多い~~まだまだ伸び代たっぷりなライブラリです．

そんなPelemayを実装する上で得られた知見を共有できればと思います．

## 動作環境

- MacOS: 10.14.6
- OTP 22/Elixir 1.9.0

# Macroモジュール
私が一番お世話になったのが， Macroモジュールです． まんまですね

このモジュールの内，

- Macro.unpipe/1 　パイプで繋がった一連の式を加工しやすいように前処理する
- Macro.prewalk/2　ElixirのASTを走査し，必要に応じてコードを書き換える補佐

上記２つの関数についてご紹介します．

## Macro.unpipe/1

論よりRunです，ひとまず下記のコードを`iex`で実行して値を確認しましょう．

```elixir
iex> ast = quote do: list |> Enum.map(&foo(&1)) |> Enum.map(&bar(&1))
```

おぞましい値が出ますね．抽象構文木といいます．
変数の`ast`は　Abstract Syntax Tree（抽象構文木）のことです.

以降，本記事で扱う変数`ast`は上記のパイプで繋がった式を指します．

```elixir
{:|>, [context: Elixir, import: Kernel],
 [
   {:|>, [context: Elixir, import: Kernel],
    [
      {:list, [], Elixir},
      {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
       [{:&, [], [{:foo, [], [{:&, [], [1]}]}]}]}
    ]},
   {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
    [{:&, [], [{:bar, [], [{:&, [], [1]}]}]}]}
 ]}

```

括弧の対応関係を読み切る必要はありません．

- パイプとかEnumとかあるし，コンパイラはこんな風に情報を受けっているんだな
- 括弧がネストしてるから再帰で走査する形っぽい？

という事だけ察してくださいませ．

それでPelemayというか， 私はこの表現をいじくるわけですがこのままだと目が疲れるのでなんとかしたい...
そこで登場するのが`Macro.unpipe`です．

名前の通り，パイプを消します．では，次のコードを実行します．

```elixir
iex> Macro.unpipe(ast)
```

少しばかりおまけがついていますが，パイプが外れてリスト構造になりました．

```elixir

[
  {{:list, [], Elixir}, 0},
  {{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
    [{:&, [], [{:foo, [], [{:&, [], [1]}]}]}]}, 0},
  {{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
    [{:&, [], [{:bar, [], [{:&, [], [1]}]}]}]}, 0}
]
```

リストということは...Enumが使えます！

つまり，メタプログラミングでもEnumが使えるということです．
（まぁ，中身は相変わらず多重のタプル`{}`があるんですが）

パイプを使ったコードをunpipeすることで得られるリストは，一番最初がデータ（変数）であることが自明になり，残りはデータ変換処理であることがわかります．

## Macro.prewalk/2, 3

Pelemayでは， `Enum.map`をサポートしていますが，サポートするためにはコードから探してくる必要があります．
そこで活躍するのが`Macro.prewalk/2`です． 

元の説明を訳すと，深さ優先行き掛け順で走査するとあります．
とりあえず動作を確認しましょう．

```elixir
iex> Macro.prewalk(ast, fn x -> x |> IO.inspect end)
```

```elixir
{:|>, [context: Elixir, import: Kernel],
 [
   {:|>, [context: Elixir, import: Kernel],
    [
      {:list, [], Elixir},
      {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
       [{:&, [], [{:foo, [], [{:&, [], [1]}]}]}]}
    ]},
   {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
    [{:&, [], [{:bar, [], [{:&, [], [1]}]}]}]}
 ]}
{:|>, [context: Elixir, import: Kernel],
 [
   {:list, [], Elixir},
   {{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
    [{:&, [], [{:foo, [], [{:&, [], [1]}]}]}]}
 ]}
{:list, [], Elixir}
{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
 [{:&, [], [{:foo, [], [{:&, [], [1]}]}]}]}
{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}
{:__aliases__, [alias: false], [:Enum]}
:Enum
:map
{:&, [], [{:foo, [], [{:&, [], [1]}]}]}
{:foo, [], [{:&, [], [1]}]}
{:&, [], [1]}
1
{{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
 [{:&, [], [{:bar, [], [{:&, [], [1]}]}]}]}
{:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}
{:__aliases__, [alias: false], [:Enum]}
:Enum
:map
{:&, [], [{:bar, [], [{:&, [], [1]}]}]}
{:bar, [], [{:&, [], [1]}]}
{:&, [], [1]}
1
```

また括弧がたくさん出てきますね．`Macro.prewalk/2`はコードの中間表現を１つずつ走査します．

この`Macro.prewalk/2`に渡す関数には，コードをどのように変更するのかを記述します．

試しに`:Enum`を`:String`に変えてみましょう．

```elixir
iex> replace = fn 
...>   :Enum -> :String
...>   other -> other
...> end
iex> Macro.prewalk(ast, &replace.(&1))
```

EnumがStringに変わりました．

```elixir
{:|>, [context: Elixir, import: Kernel],
 [
   {:|>, [context: Elixir, import: Kernel],
    [
      {:list, [], Elixir},
      {{:., [], [{:__aliases__, [alias: false], [:String]}, :map]}, [],
       [{:&, [], [{:foo, [], [{:&, [], [1]}]}]}]}
    ]},
   {{:., [], [{:__aliases__, [alias: false], [:String]}, :map]}, [],
    [{:&, [], [{:bar, [], [{:&, [], [1]}]}]}]}
 ]}

```

## Pelemayでのコード変換
実際には，パイプで繋がった一連の式を関数定義群から抽出しなくてはいけないので，`Keyword.take/2`で`do`ブロック内部の式を取ってくるといった処理が必要です．

その後，本記事で紹介した`Macro.unpipe/1`+@で前処理，`Macro.prewalk/3`で書き換えています．
unpipeした後は必然的にパイプを付け直さないといけないのですが，これはマクロとして定義されているパイプライン構文`|>`を展開するのと同じ動作で実現できます．(kernel.exから定義を引っ張ってきて少し改良)

なるべくシンプルかつ汎用的に使えるように実装しようとしているのですが，これが中々難しく手詰まっているところです．

# まとめ
Elixirでコード変換をする上で，役立つ関数についてご紹介しました．

---

明日12/5は @tuchiro さんの「[Elixir製MQTTクライアントtortoiseを使ってみた](https://qiita.com/tuchiro/items/e7fb5185f4ac191b94b5)」です．お楽しみに！