C#で言語内DSLを書くテクニック

 基本的にはメソッドチェーンで書くのがいいと思う。

 とりあえず二人の人間が会話するところを言語内言語で記述してみる。一人はJibun,もう一人はAiteとする。

Begin
.Jibun("うんこくえよ")
.Aite("やだよ")
.Jibun("だけど・・・",
       "だけどさ・・・") //複数行のテキストはカンマで区切る
.Jibun("やっぱり食えよ")
.Aite("死ねよ")
.End;

 これの裏側はこんな感じになる。

class Base{}

enum Who{ Jibun, Aite }

class Talk : Base
{
  //誰がしゃべるか
  public Who Who{ get; private set; }

  string[] Message{ get; private set; }

  public Talk(Who who, string[] message){..代入..}
}

//言語内言語はこのクラスを継承して書く
class Script
{
  //メンバごとにインターフェースを作る。
  protected interface IJibun{ ITalkResult Jibun(params string[] message); }

  protected interface IAite{ ITalkResult Aite(params string[] message); }

  protected interface IEnd{ IEndResult End{ get; } }

  //戻り値の型には、次に呼び出せるメンバを設定する
  protected interface ITalkResult : IJibun, IAite, IEnd{}

  protected interface IEndResult{}

  //実装クラスはprivateにして戻り値の型を全部持っとく。
  private class Lang : ITalkResult, IEndResult
  {
    List<Base> list = new List<Base>();

    public ITalkResult Jibun(params string[] message)
    {
      list.Add(new Talk(Who.Jibun, message);

      //自分自身を返す
      return this; 
    }

    public ITalkResult Aite(params string[] message)
    {
      list.Add(new Talk(Who.Aite, message);
      return this;
    }

    public IEndResult End{ get{ return this; } }
  }


  protected ITalkResult Begin{ get{ return new Lang(); } }
}

 実装クラスはまとめて、メソッドチェーンの構文の構成はinterfaceを使ってメソッドを取捨選択するのがいいように思う。

 人のパラメータによって実行する文を変える、if文を実装してみる。

Begin
.Jibun("うんこしにいこうぜ")
.If(() => Aite.便意 > 80, //便意が80を超えていたら
  Then => Then.Aite("そうだな")
              .Aite("一緒に行こう!!")
              .End,
  Else => Else.Aite("行かねえよ")
              .End
).End;

 ラムダ式を使うと実装が楽だ。別にこうでもいい。

Begin
.Jibun("うんこしにいこうぜ")
.If(() => Aite.便意 > 80)
  .Aite("そうだな")
  .Aite("一緒に行こう")
.Else
  .Aite("行かねえよ")
.EndIf
.End;

 だがインタプリタが多少大変になるし、間違った場所に現われたElseやEndIfをコンパイルエラーに出来るようにするのは多分原理的に無理だ(インタプリタで解析するときにやればいいのだが、それは言語内DSLとしてはコンパイルエラーとは呼べないだろう)。とりあえず前者を書いてみる。

class If : Base
{
  public Func<bool> Pred{ get; private set; }
  public Base[] Then{ get; private set; }
  public Base[] Else{ get; private set; }

  public If(...){...}
}

class Lang : ...
{
  //listを取り出してToArrayする
  Base[] array(IEndResult endResult){ return (endResult as Lang).list.ToArray(); }

  public ITalkResult If(Func<bool> pred, Func<ITalkResult,IEndResult> then,
                        Func<ITalkResult,IEndResult> @else)
  {
    list.Add(new If(pred, array(then(Begin)), array(@else(Begin))));
    return this;
  }
  ...
}

 ああ楽だ。インタプリタもコードの木構造が出来ているから楽だ。インタプリタはisの連打で書くのがいいです。