(この記事は、「fukuoka.ex(その2) Elixir Advent Calendar 2017」の7日目、個人開発 Advent Calendar 2017の16日目です)
昨日は@zacky1972さんの「ZEAM開発ログv0.1.5 Elixir から Rustler でネイティブコードベンチマークを呼び出してみよう〜ElixirでAI/MLを高速化」でした!
はじめに
この度、fukuoka.exキャストとして、「季節外れのfukuoka.ex(その2) Elixir Advent Calendar」に参加させていただくことになりました、@koga1020と申します。アドバイザーと一緒に、Elixirに関する情報をお届けしてまいります。引き続き、応援よろしくお願いいたします
本題
この記事ではElixirのパーサーコンビネータライブラリCombineの使い方を解説します。
パーサーコンビネータ??
パーサーコンビネータ = パーサーとパーサーを組み合わせてさらに大きなパーサーを作りだす関数
と解釈しています。
こちらのブログがパーサーコンビネータの概念を理解するのに良かったです。
JavaScriptでパーサコンビネータのコンセプトを理解する(「正規表現だけに頼ってはいけない」の続き) - id:anatooのブログ
小さいパーサーの入力と出力をつなぎ合わせて、大きなパーサーを作るイメージですね。
何が嬉しいのか
使いこなすと、正規表現だけで頑張らず、すっきりとしたコードを書くことができます。
Combine
ElixirのパーサーコンビネータライブラリにCombineがあります。
リポジトリ: GitHub - bitwalker/combine: A parser combinator library for Elixir projects
ドキュメント:API Reference – combine v0.10.0
インストール
公式の通り、depsに追加後、依存パッケージをインストールすれば利用できます。
# プロジェクトを作成
$ 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環境で挙動を確認することが出来ます。
iex(1)> import Elixir.Combine.Parsers.Text
Combine.Parsers.Text
iex(2)> Combine.parse("Hi", char())
["H"]
どんな風に書けるの?
公式のサンプルを元に、処理の流れを解説します。
(※ 簡略化のため、datetime_zonedの例は除いています)
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を表しています)。
左から順に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のパーサーが作れそうです。
この流れを踏まえて再度コードを見てみると、
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ではなくエラーが発生した場合に、どんな値を期待しているかを表示するためのものです。
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モジュールの関数、何にマッチさせるかはCombine.Parsers.Textモジュールの関数で書くことができます。
例. 1文字にマッチするchar()を複数回実行する
# 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」です!お楽しみに!
満員御礼!Elixir MeetUpを6月末に開催します
※応募多数により、増枠しました
「fukuoka.ex#11:DB/データサイエンスにコネクトするElixir」を6/22(金)19時に開催します。特別ゲストも迎え、非常に濃い2時間になること間違いなしです!ご興味のある方はぜひご参加ください!