これはもともとお仕事記事として書いていたのですが、いろいろ折り合いつかず流れてしまいました。
まだ書きかけですが、完成させる気もいまいち湧かなかったので、このまま公開します。
サンプルプロジェクトは
https://github.com/karino2/BParserDSLに置いておきます。
------
概要LINQとはC# 3.0 から導入された、SelectManyのシンタックスシュガーの事だ。
近年ではRxやSpracheなど、SQL以外の分野でのLINQの活用例も増えてき
た。
このシンタックスシュガーを用いてDSLを作る方法を解説する。
SelectManyを定義する事で、とても短時間で強力なDSLを構築する事が出来
る。
(以下本文)
SelectManyは非常に強力で多くの応用がある事が判明してきていて、最近の言
語では大抵言語のコアの文法要素として、このシンタックスシュガーが入っている。
C
#でもこの例に漏れずLINQという形でシンタックスシュガーが導入された。
SelectManyを使ったDSLの利点は
1. 実装コストが少ない(コアは数十行程度)
2. 柔軟性が高い
3. 拡張が容易
4. インテリセンスがフルで機能する
5. 型チェックがとても有効に機能して、大変バグが入りにくい
一方欠点は
1. 抽象度が高くどういう場合にどう実装したらいいのかを簡単なハウツーに落とせない
2. シンタックスが限定される
3. 生成されるコードの効率が想像しづらい
といった物になる。
以上の特徴から、DSLの名前の通り、特定のドメインに特化した、専用のフレームワー
クを書き捨てるのに非常に向いている。
例として、jpegのヘッダをパースする例を考えてみよう。
JPEGHeader Parse(BitStream stream) {
var byte = stream.ReadByte();
VerifyByte(byte, 0xFF);
byte = stream.ReadByte();
VerifyByte(byte, 0xD8);
while(!stream.IsEnd) {
var segmentType = stream.ReadBytes(2);
var segmentLen = stream.ReadBytes(2);
switch(segmentType) {
case 0xFFDB:
// parse DQT
//...
case 0xFFC4:
// parse DHT
//...
case 0xFF93:
// start data
}
}
この手の処理で巨大なswitch文を書くのは、良く行われる事ではあるが、問題も多
い。
一例を挙げると、各case文での処理や先頭の処理が多くなってくると関数に追い出す
事になるが、このswitch文と関数の関係が密過ぎて、両者を理解していないと読め
ない関数になりがちである。
また、処理自体が読みにくく、バグも埋め込みやすい。
これをSelectManyを用いたDSLで書く場合は、例えば以下のように書ける。
var Start = from h1 in ByteOf(0xFF)
from h2 in ByteOf(0xD8)
select 0xFFD8;
var Dqt = from segType in WordOf(0xFFDB)
from len in Word()
from tableId in Word()
from QFs in QF(tableId).Times(QFNum(len))
select new {name="DQT", tableId=tableId, QFList=Qfs};
var Dht = from segType in WordOf(0xFFC4)</div>
from len in Word()
....
var Segment = Dqt.Or(Dht).Or(......);
var Segments = Segment.Many();
var JpegParser = from startMarker in Start
from segs in Segments
from data in Datas
select new {Segments = segs, Data = data };
Sprache等のパーサーコンビネータに慣れていれば、特に説明は必要なかろう。
だが、初めて見た人にはどこを見たらいいか分からない人もいるかもしれない。
そこで実装方法に移る前に、簡単に個々の要素を見ておく。
var Start = from h1 in ByteOf(0xFF)
from h2 in ByteOf(0xD8)
select 0xFFD8;
これは、0xFFと0xD8で始まっていたら0xFFD8を返すパーサー、と読む。
var Dqt = from segType in WordOf(0xFFDB)
from len in Word()
from tableId in Word()
from QFs in QF(tableId).Times(QFNum(len))
select new {name="DQT", tableId=tableId, QFList=Qfs};
これは0xFFDBで始まっていたら、
len:16bit
tableId:16bit
QF(tableId): QFNum(len)個
並んでいて、それぞれの値をnew {name="DQT", tableId=tableId, QFList=Qfs}という匿名オブジェクトに詰めて返すパーサー、と読む。
QFやQFNumは自分で実装する必要がある。
そしてSegmentとSegmentsの定義を見ておくと、
var Segment = Dqt.Or(Dht).Or(......);
var Segments = Segment.Many();
...は記事の本題にかかわらない所なので省略した部分だ。
SegmentはDqtかDhtか、その他のセグメントの定義のOrである。
SegmentsはSegmentが繰り返して並んだ物である。
ここまで読むと分かる通り、これはJPEGのファイルフォーマットの仕様をほとんど書き下しただけで、プログラミング的な要素はほとんど含んでいない。
DSLを構築して、その上で仕様を記述する、というのは、こういう事である。
以下では、このDSLを構築する為のコードを解説していく。
LINQを用いたDSLの構築は以下の三つの手順を踏む事になる。
1. 成功か失敗かと、その他付加情報を保持しつつ結果の値を持つResultジェネリクスクラスを作る
2. SelectManyとSelectを実装する
3. 基本となるビルディングブロックを定義する
DSLのコアは2なのだが、慣れるまでは1もセットと思っておくのが簡単で良い。
以下、それぞれ見ていこう。
1. 成功か失敗かと、その他付加情報を保持しつつ結果の値を持つResultジェネリクスクラスを作る
結果の値に、パースの結果が成功だったか失敗だったか、どこまでパースしたかの付加情報をつけるResultクラスを作る。
public interface IResult<out T>
{
T Value { get; }
Input Reminder { get; }
bool WasSuccess { get; }
}
public class Result<T> : IResult<T>
{
public T Value { get; set; }
public Input Reminder { get; set; }
public bool WasSuccess { get; set; }
public static IResult<T> Success(T val, Input rem)
{
return new Result<T>() { Value = val, Reminder = rem, WasSuccess = true };
}
public static IResult<T> Fail(Input rem)
{
return new Result<T>() { Value = default(T), Reminder = rem, WasSuccess = false };
}
}
2. SelectManyとSelectを実装する
ここがDSLのコアとなる部分の実装となる。
パーサーはbinaryのパーサーという事でBParserと呼ぶ事にする。
public delegate IResult<T> BParser<out T>(Input input);
public static class BParse
{
public static BParser<U> Select<T, U>(this BParser<T> first, Func<T, U> convert)
{
return i => {
var res = first(i);
if(res.WasSuccess)
return Result<U>.Success(convert(res.Value), res.Reminder);
return Result<U>.Fail(i);
};
}
public static BParser<V> SelectMany<T, U, V>(
this BParser<T> parser,
Func<T, BParser<U>> selector,
Func<T, U, V> projector)
{
return (i) => {
var res = parser(i);
if(res.WasSuccess) {
var parser2 = selector(res.Value);
return parser2.Select(u=>projector(res.Value, u))(res.Reminder);
}
return Result<V>.Fail(i);
};
}
}
なかなか複雑なコードであるが、実装自体は30行やそこらで出来る。
SelectとSelectManyの定義はややこしく、何も見ないで書くのはなかなか大変だ。
そこで実際に実装する時は、似たような問題の実装を参考に書いてみて、型のエラーを見ながら直していく、という形になる。
何度かやってみると分かるが、型を合わそうとすれば、だいたい正しい実装が分かるようになっている。
だから、コードを読むよりはIDEの助けを借りて書いてみる方が簡単だ。
3. 基本となるビルディングブロックを定義する
ここは必要に応じて足していく部分となるので、これで終わり、という事を最初に言う事は出来ないが、まずは以下のような物を定義してみよう。
public static BParser<byte> ByteOf(Predicate<byte> predict)
{
return i =>
{
if (i.End || !predict(i.Current))
return Result<byte>.Fail(i);
return Result<byte>.Success(i.Current, i.Advance());
};
}
バイトを読んで、その結果がpredictを満たせば成功、満たさなければ失敗を返すパーサーを定義する関数だ。
以上の40行ほどの定義で、DSLのコアの定義が出来た事になる。
この実装にかかるコードの短さがSelectManyを用いたDSL作成の強力なメリットだ。
ここまでで、以下のようなコードが動く。
[TestMethod]
public void Test_ByteOf_CheckFirstTwoByte_Success()
{
var parser = from byte1 in BParse.ByteOf(x => x == 0x12)
from byte2 in BParse.ByteOf(y => y == 0x34)
from byte3 in BParse.ByteOf(_ => true)
from byte4 in BParse.ByteOf(_ => true)
select new { Byte1 = byte1, Byte2 = byte2, Byte3 = byte3, Byte4 = byte4 };
var successRes = parser(new Input(new List<byte>() { 0x12, 0x34, 0x56, 0x78 }));
Assert.IsTrue(successRes.WasSuccess);
Assert.AreEqual(0x12, successRes.Value.Byte1);
Assert.AreEqual(0x56, successRes.Value.Byte3);
Assert.IsTrue(successRes.Reminder.End);
}
[TestMethod]
public void Test_ByteOf_CheckFirstTwoByte_Fail()
{
var parser = from byte1 in BParse.ByteOf(x => x == 0x12)
from byte2 in BParse.ByteOf(y => y == 0x34)
from byte3 in BParse.ByteOf(_ => true)
from byte4 in BParse.ByteOf(_ => true)
select new { Byte1 = byte1, Byte2 = byte2, Byte3 = byte3, Byte4 = byte4 };
var successRes = parser(new Input(new List<byte>() { 0xff,0xff, 0x56, 0x78 }));
Assert.IsFalse(successRes.WasSuccess);
}
少し魔法のように感じる読者も居るだろう。そこで簡単にメカニズムを解説しておく。
from a in Parser
select FuncA;
のようなコードは、
Parser.Select((a) => FuncA(A))
に展開され、
from a in ParserA
from b in ParserB
select FuncC;
のようなコードは
ParserA.SelectMany(a=>ParserB, (a, b)=> FuncC);
のように展開される。
この二つを組わせると、
from a in ParserA
from b in ParserB
from c in ParserC
from d in ParserD
select SomeFunc(a, b, c, d);
のようなコードがSelectManyとSelectに展開される。どういう時にSelectが呼ばれてどういう時にSelectManyが呼ばれるかを意識する必要はあまり無いだろう。
いつも両方定義しておく物、と考えておく方が簡単であるし、実用上十分でもある。
以上でDSLのコアとなる部分の定義が終わった。
これだけでもかなりの表現力を持つが、実際に仕様の記述がストレートになるように、解こうとしている問題に応じてビルディングブロックを追加していく。
例えば以下のような物はjpegのパーサーにあると良いだろう。
public static readonly BParser<byte> Byte = ByteOf(_ => true);
public static BParser<byte> ByteOf(byte expect) { return ByteOf(val=>expect == val); }
public static readonly BParser<UInt16> Word =
from firstByte in Byte
from secondByte in Byte
select (UInt16) ((firstByte << 8) | secondByte);
public static BParser<UInt16> WordOf(Predicate<UInt16> predict)
{
return input =>
{
var res = Word(input);
if (!res.WasSuccess || !predict(res.Value))
return Result<UInt16>.Fail(input);
return Result<UInt16>.Success(res.Value, res.Reminder);
};
}
public static BParser<UInt16> WordOf(UInt16 expect)
{
return WordOf(val => expect == val);
}
staticなフィールドにクエリ式として定義するかstatic関数として定義するかは、定義したい物の書きやすさに応じて選べば良い。
多くのutility関数は、どちらでも定義出来る。
これらの関数で、WordOf以外は明示的にInputを操作している人がいない事に注意して欲しい。
既に定義されたDSLの上で新たな抽象を定義している為、現在Endで無いのかをチェックしたり、まだ先が読めるかどうかをチェックしたりする必要は無い。
WordOfの部分も同様にInputを触らないようにする事も可能で、その場合はWhere句を実装する事になる。
LINQの習熟度、問題の複雑さなどに応じて、好きな方を選べば良い。
以上のutilityは既に定義したDSLの上でのモジュール化だが、DSL自体を拡張したい場合もある。
最初に例で挙げたJpegのDSLではOrやManyなどがそれにあたる。
public static BParser<T> Or<T>(this BParser<T> first, BParser<T> second)
{
return i =>
{
var fr = first(i);
if (fr.WasSuccess)
return fr;
return second(i);
};
}
public static BParser<IEnumerable<T>> Times<T>(this BParser<T> parser, int num)
{
return input =>
{
var reminder = input;
var resultAll = new List<T>();
for (int i = 0; i < num; i++)
{
var resultOne = parser(reminder);
if (!resultOne.WasSuccess)
return Result<IEnumerable<T>>.Fail(reminder);
resultAll.Add(resultOne.Value);
reminder = resultOne.Reminder;
resultOne = parser(reminder);
}
return Result<IEnumerable<T>>.Success(resultAll, reminder);
};
}
public static BParser<IEnumerable<T>> Many<T>(this BParser<T> parser)
{
return i =>
{
var rem = i;
var result = new List<T>();
var oneRes = parser(rem);
while(oneRes.WasSuccess)
{
if (rem == oneRes.Reminder)
break;
result.Add(oneRes.Value);
rem = oneRes.Reminder;
oneRes = parser(rem);
};
return Result<IEnumerable<T>>.Success(result, rem);
};
}
Orは二つのパーサーをつなげて、そのどちらかが成功すれば成功とみなすパーサーを返す。
例えば以下のように使う。
var parser = parserA.Or(parserB)
ManyやTimesが少し煩雑に見えるのは、ラムダ式ではyield returnが使えないという言語の制約による。
実際にはSelectとSelectManyさえ実装してしまえば、残りの一つ一つに難しい事は無い。
DSL自身を拡張する場合はinputの現在の状態を注意しながら書かなくてはいけない分、バグを埋め込まないように注意する必要がある。
だが、非DSL版では全ての場所がそうだったのに対し、DSL版では基礎となるビルディングブロックの、少数の所だけに集中する。
だからそこを集中してテストすれば良い。
Manyは正規表現でいう所の*に相当し、一回もパースしない、という場合でもSuccessを返す。
Manyを定義するかAtLeastOneとして一回目は必ず成功するように定義するか、または両方を定義するかは問題による。
JPEGのヘッダをパースする目的ではどちらでも構わないので、実装が簡単なManyを実装した。
ここまでで、簡単にヘッダの内容を表示するコードを書いてみる事が出来る。
static readonly BParser<UInt16> Start = from h1 in BParse.ByteOf(0xFF)
from h2 in BParse.ByteOf(0xD8)
select (UInt16)0xFFD8;
static readonly BParser<Dictionary<string, object>> SOSSegment =
from segType in BParse.WordOf(0xFFDA)
from len in BParse.Word
from data in BParse.Byte.Times(len - 2)
select new Dictionary<string, object>() { { "Type", segType }, { "Length", len } };
static readonly BParser<Dictionary<string, object>> GenericSegment =
from segType in BParse.WordOf(val => val!= 0xFFDA)
from len in BParse.Word
from data in BParse.Byte.Times(len-2)
select new Dictionary<string, object>() { {"Type", segType}, {"Length", len} };
static readonly BParser<IEnumerable<Dictionary<String, object>>>
JpegParser = from startMarker in Start
from segs in GenericSegment.Many()
from sosseg in SOSSegment
select Add(segs, sosseg);
public static IEnumerable<T> Add<T>(IEnumerable<T> source, T item)
{
foreach(var val in source)
yield return val;
yield return item;
}
static void Main(string[] args)
{
using (var reader = new FileStream("test.jpg", FileMode.Open))
using (var binReader = new BinaryReader(reader))
{
var bytes = binReader.ReadBytes((int)reader.Length).ToList();
var res = JpegParser(new Input(bytes));
if (!res.WasSuccess)
throw new Exception("parse fail");
foreach (var seg in res.Value)
{
Console.WriteLine(String.Format("Type={0:X}", seg["Type"]));
Console.WriteLine(String.Format("Len={0}", seg["Length"]));
}
}
}
JPEGのヘッダの最後のセグメントがSOSであるという事を用いる為に、SOS Segmentとそれ以外のSegmentという定義をしている。
Geneirc Segmentのsegment typeの部分の定義に着目してみよう。
static readonly BParser<Dictionary<string, object>> GenericSegment =
from segType in BParse.WordOf(val => val!= 0xFFDA)
...
Generic Segmentの定義にはラムダ式として0xFFDA以外のヘッダなら、というコードが書かれている。
コードの部分は仕様記述というよりはプログラム的なので、バグの入る余地も多くなるが、この位ならラムダ式で書いてしまっても構わないと思う。
そこは問題の複雑さに応じて、例えば
public static readonly BParser<UInt16> WordExceptFor(UInt 16 except) = WordOf(val => val != except);
などを定義して、WordExceptFor(0xFFDA)と書いても良い。
DSLを拡張していくか、DSL内で書いていく方がいいかは、解いている問題による。
好きな方を選べば良いだろう。
そしてヘッダの最後はSOSで終わる、という制約は以下の部分に書かれている。
static readonly BParser<IEnumerable<Dictionary<String, object>>>
JpegParser = from startMarker in Start
from segs in GenericSegment.Many()
from sosseg in SOSSegment
select Add(segs, sosseg);
Startがまず来て、次にGenericSegmentが複数回繰り返されて、最後にSOSSegmentが来る、と読む事が出来る。
segsがIEnumerableでsossegが単一の要素なので、その二つを足し合わせる為にAddというstaticメソッドを作った。
ここまでの簡単な実装で、かなりの表現力を持ったDSLになっている。
例えば以下のTimesに注目して欲しい。
static readonly BParser<Dictionary<string, object>> SOSSegment =
from segType in BParse.WordOf(0xFFDA)
from len in BParse.Word
from data in BParse.Byte.Times(len - 2)
select new Dictionary<string, object>() { { "Type", segType }, { "Length", len } };
最初のlenの値に応じてdataの長さが決まっている。
このような物は、簡易な外部DSLの実装の場合、対応が難しい事もある。
だがSelectManyを用いたDSLではとても自然に扱う事が出来ている。