はじめに
この記事はSansan Advent Calendar 2018の12日目の記事です。
実は昨日までいろいろあって執筆したのは実は今日の昼です。マジで反省します。
何で Parser なんて書こうと思ったか
Azure の ARM Templete みたいにリソースの定義とその中で使えるBulit inの関数みたいなものが
プロジェクトで必要になりそうだったので、人知れず調べていました。(結果的にいらなくなったけど)
で Sprache というものがあると知ったが…
Spracheというスペル書けるけど読めないライブラリがあることを知って使ってみたけど、下記の理由でどハマってしまいました…。
- 関数型チックで全然ピンとこない
- 私は関数型言語の経験がない
- サンプルあったけど、私には高尚すぎて理解できなかった
- サンプルの計算機があるんですが、いろいろとやりすぎてて理解に時間がかかってしまった
というわけで
同じ悩みを抱えちゃってるかもしれない人向けに記事書いてみることにしました。
とりあえずソースコードを晒す
解説
例題
以下のような文字列を関数としてパースすることを考えます。
"Coalesce(Concat($.data.property ,b) ,Concat(c ,d))"
コード
肝はコード中でも↓のParser定義部分なのですが、実は一番わかりにくいっていう(関数型言語のパラダイムでできている)
なので、コード内にコメントを入れつつ解説します
private static readonly Parser<Expression> Function =
// 関数名の部分 関数名にカッコやカンマは含まれないはずなので除外する
from name in Parse.CharExcept(new[] { '(', ')', ',' }).AtLeastOnce().Text()
// 関数の最初のカッコ
from lparen in Parse.Char('(')
// 関数の引数の部分 引数に関数を取る場合があり得るので`Ref`を使用して再帰的にパースされるように定義する
from arguments in Parse.Ref(() => Arguments).DelimitedBy(Parse.Char(',').Token())
// 関数の閉じカッコ
from rparen in Parse.Char(')')
// 関数を表す文字列と引数を表す文字列をExpressionにする関数を呼び出す
select CallFunction(name, arguments.ToArray());
// 引数のうち、関数でないもの(=定数)
private static readonly Parser<Expression> Constant =
Parse.CharExcept(new[] { '(', ')', ',' }).AtLeastOnce().Token().Text().Select(Expression.Constant);
// 引数は関数か定数であるはず。なので、まず関数かどうかを評価してから関数でない場合は定数と判断する(なので`Or`を利用している)
private static readonly Parser<Expression> Arguments = Function.Or(Constant);
これを理解できればサンプルの計算機が一体何をしているかわかるようになる…はずです。
終わりに
Parserを書くことなんてそうそうない気がしますが
「ある決まりを持って構造的に記述されたテキスト」なんかを処理するときにはとても有効です。
XMLやJSONであれば吐いて捨てるほどParserはありますが、ちょっとニッチなものや、
自社のシステムにしかないようなものだと自作するしかありません。
そんな時に便利…かもしれません。