DSL / Foreign Code


Foreign Code

一言要約

DSLでできることよりも複雑な振る舞いを提供するのに、外部のコードを埋め込む。

要約

仕組み

他の言語を組み入れることは二つの疑問を伴う。

  • どのようにして分別し、どのように文法に織り込むのか。
  • どのようにしてやりたいことを実行できるのか。

大抵の場合Alternative Tokenizationを使用する必要があり、また一つの長い文字列としてパーサでそれを読み取る必要がある。

セマンティックモデルの中にそのままの形式で文字列を埋めることもでき、またより複雑ではあるがForeign Code用のパーサを介してセマンティックモデルにより多くのものを埋め込められる。

次のステップはForeign Codeをインタプリタ言語とするかコンパイル言語とするかを決めること。

ホスト言語がインタプリタ言語と相互運用するメカニズムがあるなら、インタプリタ言語を選択したほうが容易。

ホスト言語がインタプリタ言語であればいうまでもない。

ホスト言語がコンパイル言語であればデータのやり取りをどうするか考慮すべし。

DSL以外の別の言語を導入することになるので問題となることも。

ホスト言語がコンパイル言語であれば、ホスト言語自身を埋め込むのもあり。

ビルドプロセスにForeign Codeのコンパイルのステップを追加する。

Foreign Codeを使うときは埋め込みヘルパー(Embedment Helper)を使うことを考えろ。

Foreign Codeを使うとDSLの可読容易性が失われるが、埋め込みヘルパーで軽減できるっぽい。(???解釈に自信がないです)

Foreign CodeがDSLで定義されるシンボルを参照する必要がある場合、パーサジェネレータはこれらの参照を解決すべき。

使いどころ

Foreign Codeを使うことの代案として、DSLそのものを拡張する、というものもある。

悪い面としては次のものがあげられる。

  • DSLの与える抽象概念が破られる。
  • DSLと同様にForeign Codeを理解する必要がある。
  • セマンティックモデル自身も複雑になる。

本当に汎用言語が必要な場合に使え!(DSLを汎用言語になんか変えたくないよね)

誰が使うかも考慮する必要がある。

  • プログラマが使うならForeign Codeなんて簡単に理解できる。
  • 非プログラマが使うならちょっと考えよう。

Antlr、Java、JavaScript?の例

セールスマンに、製品グループおよび地域の販売指揮権を割り当てる例。

scott handles floor_wax in WA;
helen handles floor_wax desert_topping in AZ NM;
brian handles desert_topping in WA OR ID MT;
otherwise scott

Scottが南ニューイングランドで営業するBaker Industriesの経営者と仲良くなったので、フロアワックスも含めてBaker Industriesへの指揮権を割り当てたい。
が、Baker Industriesは「Baker Industrial Holdings」とか「Baker Floor Toppings」とかいろいろ名前がある。
要するに頭が「Baker」で始まる会社であればよいのだが、DSLを拡張するよりはForeign Codeでやってしまえと。

scott handles floor_wax in MA RI CT when {/^Baker/.test(lead.name)};

セマンティックモデル
leadAllocationCD.jpg

指揮権

class Lead...
  private String name;
  private State state;
  private ProductGroup product;

  public Lead(String name, State state, ProductGroup product) {
    this.name = name;
    this.state = state;
    this.product = product;
  }

  public State getState() {return state;}
  public ProductGroup getProduct() {return product;}
  public String getName() {return name;}

セールスマンと指揮権スペックを結びつける指揮権アロケータ

class LeadAllocator...
  private List<LeadAllocatorItem> allocationList = new ArrayList<LeadAllocatorItem>();
  private Salesman defaultSalesman;

  public void appendAllocation(Salesman salesman, LeadSpecification spec) {
    allocationList.add(new LeadAllocatorItem(salesman, spec));
  }

  public void setDefaultSalesman(Salesman defaultSalesman) {
    this.defaultSalesman = defaultSalesman;
  }

  private class LeadAllocatorItem {
    Salesman salesman;
    LeadSpecification spec;

    private LeadAllocatorItem(Salesman salesman, LeadSpecification spec) {
      this.salesman = salesman;
      this.spec = spec;
    }
  }

指揮権スペック

class LeadSpecification...
  private List<State> states = new ArrayList<State>();
  private List<ProductGroup> products = new ArrayList<ProductGroup>();
  private String predicate;

  public void addStates(State... args) {states.addAll(Arrays.asList(args));}
  public void addProducts(ProductGroup... args) {products.addAll(Arrays.asList(args));}
  public void setPredicate(String code) {predicate = code;}

  public boolean isSatisfiedBy(Lead candidate) {
    return statesMatch(candidate)
        && productsMatch(candidate)
        && predicateMatches(candidate)
    ;
  }
  private boolean productsMatch(Lead candidate) {
    return products.isEmpty() || products.contains(candidate.getProduct());
  }
  private boolean statesMatch(Lead candidate) {
    return states.isEmpty() || states.contains(candidate.getState());
  }
  private boolean predicateMatches(Lead candidate) {
    if (null == predicate) return true;
    return evaluatePredicate(candidate);
  }

Rhinoを使ってJavascriptを評価

class LeadSpecification...
  boolean evaluatePredicate(Lead candidate) {
    try {
      ScriptContext newContext = new SimpleScriptContext();
      Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE);
      engineScope.put("lead", candidate);
      return (Boolean) javascriptEngine().eval(predicate, engineScope);
    } catch (ScriptException e) {
      throw new RuntimeException(e);
    }
  }
  private  ScriptEngine javascriptEngine() {
    ScriptEngineManager factory = new ScriptEngineManager();
    ScriptEngine result = factory.getEngineByName("JavaScript");
    assert result != null : "Unable to find javascript engine";
    return result;
  }

指揮権アロケータは最初に条件にマッチしたセールスマンを選択

class LeadAllocator...
  public Salesman determineSalesman(Lead lead) {
     for (LeadAllocatorItem i : allocationList)
       if (i.spec.isSatisfiedBy(lead)) return i.salesman;
     return defaultSalesman;
   }

パーサ

メインのクラスであるアロケーショントランスレータ

class AllocationTranslator...
  private Reader input;
  private allocationLexer lexer;
  private allocationParser parser;
  private ParsingNotification notification = new ParsingNotification();
  private LeadAllocator result = new LeadAllocator();

  public AllocationTranslator(Reader input) {
    this.input = input;
  }
  public void run() {
    try {
      lexer = new allocationLexer(new ANTLRReaderStream(input));
      parser = new allocationParser(new CommonTokenStream(lexer));
      parser.helper = this;
      parser.allocationList();
    } catch (Exception e) {
      throw new RuntimeException("Unexpected exception in parse", e);
    }
    if (notification.hasErrors()) throw new RuntimeException("Parse failed: \n" + notification);
  }

アロケーショントランスレータは埋め込みヘルパー役を担う

grammar...
  @members {
    AllocationTranslator helper;
 
    public void reportError(RecognitionException e) {
      helper.addError(e);
      super.reportError(e);
    }
  }

コアのトークン定義

grammar...
  ID  : ('a'..'z' | 'A'..'Z' | '0'..'9' | '_' )+;
  WS  : (' ' |'\t' | '\r' | '\n')+ {skip();} ;
  SEP : ';';

文法のトップレベルの定義

grammar...
  allocationList 
    : allocationRule* 'otherwise' ID {helper.recognizedDefault($ID);}
    ;

class AllocationTranslator...
  void recognizedDefault(Token token) {
    if (!Registry.salesmenRepository().containsId(token.getText())) {
         notification.error(token, "Unknown salesman: %s", token.getText());
         return;
       }
    Salesman salesman = Registry.salesmenRepository().findById(token.getText());
    result.setDefaultSalesman(salesman);
  }

アロケーションルール

grammar...
  allocationRule
    : salesman=ID  pc=productClause lc=locationClause ('when' predicate=ACTION)? SEP 
        {helper.recognizedAllocationRule(salesman, pc, lc, predicate);}
    ;

プロダクト節

grammar...
  productClause  returns [List<ProductGroup> result]
    : 'handles' p+=ID+ {$result = helper.recognizedProducts($p);}
    ;

トークンをプロダクトオブジェクトにコンバート

class AllocationTranslator...
  List<ProductGroup> recognizedProducts(List<Token> tokens) {
    List<ProductGroup> result = new ArrayList<ProductGroup>();
     for (Token t : tokens) {
       if (!Registry.productRepository().containsId(t.getText())) {
         notification.error(t, "No product for %s", t.getText());
         continue;
       }
       result.add(Registry.productRepository().findById(t.getText()));
     }
     return result;
  }

Alternative Tokenizerのもっとも単純な例

ACTION : '{' .* '}' ;

Javascript内にカーリーブラケットがあると失敗するので、二つの文字を組み合わせた例

ACTION : '{:' .* ':}' ;

Antlrではネストさせて定義できる

grammar...
  ACTION : NESTED_ACTION;
  
  fragment NESTED_ACTION  
    :  '{' (ACTION_CHAR | NESTED_ACTION)* '}'
    ;    
  fragment ACTION_CHAR 
    : ~('{'|'}')
    ;

サブ節(セールスマン、製品グループ、州)のコレクションとJavascriptの術語でセマンティックモデルを更新

class AllocationTranslator...
  void recognizedAllocationRule(Token salesmanName, List<ProductGroup> products, List<State> states, Token predicate) {
    if (!Registry.salesmenRepository().containsId(salesmanName.getText())) {
      notification.error(salesmanName, "Unknown salesman: %s", salesmanName.getText());
      return;
    }
    Salesman salesman = Registry.salesmenRepository().findById(salesmanName.getText());
    LeadSpecification spec = new LeadSpecification();
    spec.addStates((State[]) states.toArray(new State[states.size()]));
    spec.addProducts((ProductGroup[]) products.toArray(new ProductGroup[products.size()]));
    if (null != predicate) spec.setPredicate(predicate.getText());
    result.appendAllocation(salesman, spec);
  }

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

担当者のつぶやき

自分にとっては英語も内容もハードルが高すぎでした。。。orz
Embedment Helperも現時点ではよくわからないっす。

みんなの突っ込み