Elixirのパーサーコンビネータライブラリ Combine入門

（この記事は、[「fukuoka.ex(その2) Elixir Advent Calendar 2017」](https://adventar.org/calendars/2871)の7日目、[個人開発 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/private-development)の16日目です）

昨日は@zacky1972さんの[「ZEAM開発ログv0.1.5 Elixir から Rustler でネイティブコードベンチマークを呼び出してみよう〜ElixirでAI/MLを高速化」](https://qiita.com/zacky1972/items/95d898c5b44fd6f243ee)でした！

## はじめに
この度、fukuoka.exキャストとして、[「季節外れのfukuoka.ex(その2) Elixir Advent Calendar」](https://adventar.org/calendars/2871)に参加させていただくことになりました、@koga1020と申します。アドバイザーと一緒に、Elixirに関する情報をお届けしてまいります。引き続き、応援よろしくお願いいたします:bow:

## 本題
この記事ではElixirのパーサーコンビネータライブラリCombineの使い方を解説します。

### パーサーコンビネータ？？

パーサーコンビネータ = `パーサーとパーサーを組み合わせてさらに大きなパーサーを作りだす関数` と解釈しています。

こちらのブログがパーサーコンビネータの概念を理解するのに良かったです。
[JavaScriptでパーサコンビネータのコンセプトを理解する(「正規表現だけに頼ってはいけない」の続き) - id:anatooのブログ](http://blog.anatoo.jp/entry/2015/04/26/220026)

小さいパーサーの入力と出力をつなぎ合わせて、大きなパーサーを作るイメージですね。

### 何が嬉しいのか
使いこなすと、正規表現だけで頑張らず、すっきりとしたコードを書くことができます。

## Combine
ElixirのパーサーコンビネータライブラリにCombineがあります。

リポジトリ: [GitHub - bitwalker/combine: A parser combinator library for Elixir projects](https://github.com/bitwalker/combine)
ドキュメント:[API Reference – combine v0.10.0](https://hexdocs.pm/combine/api-reference.html#content)

## インストール
公式の通り、depsに追加後、依存パッケージをインストールすれば利用できます。

```bash
# プロジェクトを作成
$ mix new combine_practice

# フォルダに移動
$ cd combine_practice

# mix.exsを編集
$ vim mix.exs
---------------------------
  depsにcombineを追加
  defp deps do
    [
      {:combine, "~> 0.10.0"}
    ]
  end
---------------------------
# 依存パッケージのインストール
$ mix deps.get

# 依存パッケージをコンパイルしてiexを起動
$ iex -S mix
```

iex環境で挙動を確認することが出来ます。

```elixir
iex(1)> import Elixir.Combine.Parsers.Text
Combine.Parsers.Text
iex(2)> Combine.parse("Hi", char())
["H"]
```
## どんな風に書けるの？
公式のサンプルを元に、処理の流れを解説します。
（※ 簡略化のため、datetime_zonedの例は除いています）

```elixir
defmodule DateTimeParser do
  use Combine

  def parse(datetime), do: Combine.parse(datetime, parser())

  defp parser, do: date() |> ignore(char("T")) |> time()

  defp date do
    label(integer(), "year")
    |> ignore(char("-"))
    |> label(integer(), "month")
    |> ignore(char("-"))
    |> label(integer(), "day")
  end

  defp time(previous) do
    previous
    |> label(integer(), "hour")
    |> ignore(char(":"))
    |> label(integer(), "minute")
    |> ignore(char(":"))
    |> label(float(), "seconds")
    |> map(char("Z"), fn _ -> "UTC" end)
  end
end

...> datetime = "2014-07-22T12:30:05.0002Z"
...> DateTimeParser.parse(datetime)
[2014, 7, 22, 12, 30, 5.0002, "UTC"]
```

`"2014-07-22T12:30:05.0002Z"` という文字列をパースして、 `[2014, 7, 22, 12, 30, 5.0002, "UTC"]` というリストに変換していますね。


`"2014-07-22T12:30:05.0002Z"`という文字は、↓↓の図のように分解できます（ZはUTCを表しています）。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/99116/c9c02c18-6e02-68b4-26b4-809776654301.png)

左から順にparseされる流れを日本語で整理してみると、

```
2014-07-22T12:30:05.0002Z
→ 日付部分を取り出す（2014-07-22）
→ 文字"T"を無視する
→ 時刻部分を取り出す（12:30:05.0002）
→ 文字"Z"を取り出して"UTC"に置き換える
```
こんな感じになりますね。日付部分と時刻部分のパースをさらに細かく書くと、

```
# 日付部分の処理
2014-07-22
→ 整数を取り出す（結果: 2014）
→ 文字"-"を無視する
→ 整数を取り出す（結果: 7）
→ 文字"-"を無視する
→ 整数を取り出す（結果: 22）

# 時刻部分の処理
12:30:05.0002
→ 整数を取り出す（結果: 12）
→ 文字":"を無視する
→ 整数を取り出す（結果: 30）
→ 文字":"を無視する
→ 小数を取り出す（結果: 5.0002）
```

こんな感じです。日付(=date)のparserと時刻(=time)のパーサーを組み合わせて、datetimeのパーサーが作れそうです。

この流れを踏まえて再度コードを見てみると、

```elixir
defmodule DateTimeParser do
  use Combine

  # Combine.parse(<パースしたい文字>, <parser>)
  def parse(datetime), do: Combine.parse(datetime, parser())
  
  # Combine.parse()で利用するパーサーを定義する
  # 日付のparse → Tを無視 → 時刻のparse を実行  
  defp parser, do: date() |> ignore(char("T")) |> time()

  # 日付をパースするdateパーサーを定義
  defp date do
    label(integer(), "year")
    |> ignore(char("-"))
    |> label(integer(), "month")
    |> ignore(char("-"))
    |> label(integer(), "day")
  end

  # 時刻をパースするtimeパーサーを定義
  defp time(previous) do
    previous
    |> label(integer(), "hour")
    |> ignore(char(":"))
    |> label(integer(), "minute")
    |> ignore(char(":"))
    |> label(float(), "seconds")
    |> map(char("Z"), fn _ -> "UTC" end)
  end
end
```

日本語で整理したparseの流れが直感的に記述できています。このようにパイプでつなげる記述はElixirならではの記述ですね。

ちなみに`label()`はパースしたい文字が意図したformatではなくエラーが発生した場合に、どんな値を期待しているかを表示するためのものです。

```elixir
iex(6)> Combine.parse("abc", integer(), "year")
{:error, "Expected integer but found `a` at line 1, column 1"}

iex(7)> Combine.parse("abc", label(integer(), "year"))
{:error, "Expected `year` at line 1, column 1."}
```

パースを**どのように**実行するかは[Combine.Parsers.Baseモジュール](https://hexdocs.pm/combine/Combine.Parsers.Base.html#summary)の関数、**何**にマッチさせるかは[Combine.Parsers.Textモジュール](https://hexdocs.pm/combine/Combine.Parsers.Text.html#summary)の関数で書くことができます。

例. 1文字にマッチするchar()を複数回実行する

```elixir
# many()・・・Combine.Parsers.Baseの関数
# char()・・・Combine.Parsers.Textの関数
iex> import Elixir.Combine.Parsers.Base
...> import Combine.Parsers.Text
...> Combine.parse("abc", many(char()))
[["a", "b", "c"]]
```

ドキュメントにはExampleもしっかり掲載されていますので、各関数のExampleの挙動を追ってみるとすぐに覚えられると思います。

## まとめ
ElixirのパーサーコンビネータライブラリCombineの紹介でした。関数型言語らしく、つらつらと処理の流れをパイプで書けると可読性も高く、なにより楽しいです笑

パターンマッチのおかげで通常の正規表現の記述もしやすいElixirですが、パーサーコンビネータでの実装も知っておくと良さそうです。

次回は@kobatakoさんの「[ElixirでSlack Botを作った with Qiita API](https://qiita.com/kobatako/items/586f82b2845f7d1583ce)」です！お楽しみに！

---------------------------
:stars::stars::stars::stars::stars:<b>　満員御礼！Elixir MeetUpを6月末に開催します　</b>:stars::stars::stars::stars::stars:
<font color="red"><b>※応募多数により、増枠しました</b></font>
[<b>「fukuoka.ex#11：DB／データサイエンスにコネクトするElixir」</b>](https://fukuokaex.connpass.com/event/87241)を6/22（金）19時に開催します。特別ゲストも迎え、非常に濃い2時間になること間違いなしです！ご興味のある方はぜひご参加ください！



[![image.png](https://connpass-tokyo.s3.amazonaws.com/thumbs/cb/13/cb130a03343bc310b07244c308b940db.png)](https://fukuokaex.connpass.com/event/87241)