DSL / Parse Tree Manipulation


DSL

パースツリーの操作

一言要約

コードの断片を受け取ってパースツリーを構築し、それをDSLで操作する。

要約

仕組み

  • コードフラグメントを処理できるプログラミング言語は少ないし、あってもあまり使われることはない。
    • ここでは、C#、Ruby、Lispを例にとりあげる。
  • C#とRubyで行われるのは以下の処理。
    • ソースコードの断片(フラグメント)についてライブラリ呼び出しを実行する。
    • ライブラリはそのコードのパースツリーがもつデータ構造を返す。
  • C#の場合、返されるデータ構造は式オブジェクトのヒエラルキーであり、Rubyの場合はネストされたRubyの配列。
  • どちらの場合も、パースツリーを操作するクラスを書く必要がある。
  • Lispのアプローチはこれらとは異なり、マクロを使うが、得られる結果は一緒。
  • パースツリーを走査する場合には、不正なエレメントを検出しエラーにすることが必要。

使いどころ

  • 内部DSL以上に、ホスト言語を自由に駆使したい場合。
  • 特にホスト言語の式や構造を操作したい場合に用いられる。
  • 実際には。使いどころはそれほど多くない。
    • ここでは、.NETのLinqをとりあげる。
  • Linqによって、検索条件を.NETで表現することができるようになる。
    • これにより表現と操作対象を切り離すことができる。
  • パースツリーの使いどころとしては、パースツリーの変換によって何らかの処理を行うというもの。
    • 例えば、あるオブジェクトに対する処理を別のオブジェクトにリダイレクトする。

例:C#の条件からIMAPのクエリを生成する

IMAPの検索コマンド

 SEARCH subject "entity framework" sentsince 23-jun-2008 not from "@ayende.com"

C#のDSLイメージ

var threshold = new DateTime(2008, 06, 23);
     var builder = new ImapQueryBuilder((q) => 
       (q.Subject == "entity framework") 
       && (q.Date >= threshold) 
       && ("@ayende.com" != q.From));

意味論モデル

クエリオブジェクト

class ImapQuery...
   internal List<ImapQueryElement> elements = new List<ImapQueryElement>();
   public void AddElement(ImapQueryElement element) {
     elements.Add(element);
   }

クエリオブジェクトに渡されるインタフェースとその実装

interface ImapQueryElement {
   string ToImap(); 
 }
class BasicElement : ImapQueryElement {
   private readonly string name;
   private readonly object value;
   public BasicElement(string name, object value) {
     this.name = name.ToLower();
     this.value = value;
     validate().AssertOK();
   }
class NegationElement : ImapQueryElement {
   private readonly BasicElement child;
   public NegationElement(BasicElement child) {
     this.child = child;
   }

これを用いたクエリモデルの表現

     var expected = new ImapQuery();
     expected.AddElement(new BasicElement("subject", "entity framework"));
     expected.AddElement(new BasicElement("since", new DateTime(2008, 6, 23)));
     expected.AddElement(new NegationElement(
       new BasicElement("from", "@ayende.com")));

IMAPクエリの生成

class ImapQuery...
   public string ToImap() {
     var result = "";
     foreach (var e in elements) result += e.ToImap();
     return result.Trim();
   }
class BasicElement...
   public string ToImap() {
     return String.Format("{0} {1} ", name, imapValue);
   }
   private string imapValue {
     get {
       if (value is string) return "\"" + value + "\"";
       if (value is DateTime) return imapDate((DateTime)value);
       return "";
     }
   }
   private string imapDate(DateTime d) {
     return d.ToString("dd-MMM-yyyy");
   }
class NegationElement...
   public string ToImap() {
     return String.Format("not {0}", child.ToImap());
   }

C#での構築

ビルダはコンストラクタでlambdaをとる。

class ImapQueryBuilder...
    private Expression<Func<ImapQueryCriteria, bool>> lambda;
    public ImapQueryBuilder(Expression<Func<ImapQueryCriteria, bool>> func) {
      lambda = func;
    }
class ImapQueryBuilder...
    internal class ImapQueryCriteria {
      public string Subject {get { return ""; }}
      public string To {get { return ""; }}
      public DateTime Sent {get { return DateTime.Now; }}
      public string From {get { return ""; }}
class ImapQueryBuilder...
    public ImapQuery Content {
      get {
        if (null == content) {
          content = new ImapQuery();
          populateFrom(lambda.Body);
        }
        return content;
      }
    }
    private ImapQuery content;

populateFromが肝。再帰的にツリーをたどっている。

class ImapQueryBuilder...
    private void populateFrom(Expression e) {
      var node = e as BinaryExpression;
      if (null == node)
        throw new BuilderException("Wrong node class", node);
      if (e.NodeType == ExpressionType.AndAlso) {
        populateFrom(node.Left);
        populateFrom(node.Right);
      }
      else 
        content.AddElement(new ElementBuilder(node).Content);
    }

サンプル実装のふりかえり

説明と構築方法の違いについて。説明の部分ごとに実装を分けたが、実際の構築時にはその方法はとらなかった。

IMAP DSLの構築において解析木の走査を行わなかった。代替としてMethod Chainingを利用。

class Tester...
      var builder = new ChainingBuilder()
        .subject("entity framework")
        .not.from("@ayende.com")
        .since(threshold);

ビルダの実装は以下の通り。

class ChainingBuilder...
    private readonly ImapQuery content = new ImapQuery();
    private bool currentlyNegating = false;

    public ImapQuery Content {
      get { return content; }
    }
    public ChainingBuilder not {
      get { 
        currentlyNegating = true;
        return this; }
    }
    private void addElement(string keyword, object value) {
      ImapQueryElement element = new BasicElement(keyword, value);
      if (currentlyNegating) {
        element = new NegationElement((BasicElement) element);
        currentlyNegating = false;
      }
      content.AddElement(element);    
    }
    public ChainingBuilder subject(string s) {
      addElement("subject", s);
      return this;
    }
    public ChainingBuilder since(DateTime t) { 
      addElement("since", t);
      return this;
    }
    public ChainingBuilder from(string s) { 
      addElement("from", s);
      return this;
    }

このようにした理由の一つは内部DSLの構造がIMAPクエリに似ている方がシンプルになるから。

ファウラーへのフィードバック

  • C#コードのメソッドがPascalCase?じゃないのでJavaっぽく見える

担当者のつぶやき

  • C#が機能豊富でうらやましす
  • なかなか頭が関数型言語に対応できません。一回何か作ってみないとダメですね。(W)

みんなの突っ込み