DSL / Templated Generation


DSL

テンプレート生成

一言要約

出力ファイルを用意し、変更したい部分を置き換えて出力ファイルを生成する。

要約

基本的な考えは、変化させたい部分を呼び出し(call-outs)によって置き換え、出力ファイルを書きだす。

Templated Generationは非常に古く身近なテクニックで、Web開発にもよく利用されている。Cのprintf関数やテキストマクロのプリプロセッサもTemplated Generationの一つ。Templated GenerationとTransformer Generationは混同しやすい。

Templated Generationには3つの主要コンポーネントがある。出力ファイルのソーステキストを表すテンプレート、テンプレートを生成する際のデータ構造を表すコンテキスト、テンプレートとコンテキストから出力を生成するツールであるテンプレートエンジン

最も一般的な形式のテンプレートプロセッサは、JSPやASPのように、呼び出しによって置き換えられる任意のホストコード表現を可能にする。呼び出し内では、可能なかぎりEmbedment Helperを使ってシンプルな関数呼び出しのみに制限すること。

カオスを防ぐため、多くのテンプレートプロセッサは任意のホストコードを許可しておらず、テンプレート言語を提供している。

基本はmapデータ構造をコンテキストとして扱うが、繰り返し(loop)や条件(if)、サブルーチンが必要になっていくケースも多い。

いつ使用するか

Templated Generationの最大の強みは、テンプレートファイルを見ることができ、生成された出力がどのようになるかが理解しやすいこと。

Templated Generationを使う最初の指標は、生成ファイル内に静的コンテンツが沢山あるかどうか。2つ目は動的コンテンツの複雑度で、複雑度を増していくとTransformer Generationを検討すべき。

例:ネストされた条件での「秘密のパネル」ステートマシン生成(VelocityとJavaで生成するC)

例はModel Ignorant Generationで取りあげたコードを生成することにする。

#define EVENT_doorClosed "D1CL"
#define EVENT_drawOpened "D2OP"
#define EVENT_lightOn "L1ON"
#define EVENT_doorOpened "D1OP"
#define EVENT_panelClosed "PNCL"

#define STATE_idle 1
#define STATE_active 0
#define STATE_waitingForDraw 3
#define STATE_unlockedPanel 2
#define STATE_waitingForLight 4

#define COMMAND_lockDoor "D1LK"
#define COMMAND_lockPanel "PNLK"
#define COMMAND_unlockPanel "PNUL"
#define COMMAND_unlockDoor "D1UL"

static int current_state_id = -99;

void init_controller() {
  current_state_id = STATE_idle;
}

void hard_reset() {
  init_controller();
}

void handle_event_while_idle (char *code) {
  if (0 == strcmp(code, EVENT_doorClosed)) {
    current_state_id = STATE_active;
  }
  if (0 == strcmp(code, EVENT_doorOpened)) {
    current_state_id = STATE_idle;
    send_command(COMMAND_unlockDoor);
    send_command(COMMAND_lockPanel);
  }
}
void handle_event_while_active (char *code) {
  if (0 == strcmp(code, EVENT_lightOn)) {
    current_state_id = STATE_waitingForDraw;
  }
  if (0 == strcmp(code, EVENT_drawOpened)) {
    current_state_id = STATE_waitingForLight;
  }
  if (0 == strcmp(code, EVENT_doorOpened)) {
    current_state_id = STATE_idle;
    send_command(COMMAND_unlockDoor);
    send_command(COMMAND_lockPanel);
  }
}
void handle_event_while_waitingForDraw (char *code) {
  if (0 == strcmp(code, EVENT_drawOpened)) {
    current_state_id = STATE_unlockedPanel;
    send_command(COMMAND_unlockPanel);
    send_command(COMMAND_lockDoor);
  }
  if (0 == strcmp(code, EVENT_doorOpened)) {
    current_state_id = STATE_idle;
    send_command(COMMAND_unlockDoor);
    send_command(COMMAND_lockPanel);
  }
}
void handle_event_while_unlockedPanel (char *code) {
  if (0 == strcmp(code, EVENT_panelClosed)) {
    current_state_id = STATE_idle;
    send_command(COMMAND_unlockDoor);
    send_command(COMMAND_lockPanel);
  }
  if (0 == strcmp(code, EVENT_doorOpened)) {
    current_state_id = STATE_idle;
    send_command(COMMAND_unlockDoor);
    send_command(COMMAND_lockPanel);
  }
}
void handle_event_while_waitingForLight (char *code) {
  if (0 == strcmp(code, EVENT_lightOn)) {
    current_state_id = STATE_unlockedPanel;
    send_command(COMMAND_unlockPanel);
    send_command(COMMAND_lockDoor);
  }
  if (0 == strcmp(code, EVENT_doorOpened)) {
    current_state_id = STATE_idle;
    send_command(COMMAND_unlockDoor);
    send_command(COMMAND_lockPanel);
  }
}

void handle_event(char *code) {
  switch(current_state_id) {
  case STATE_idle: {
    handle_event_while_idle (code);
    return;
  }
  case STATE_active: {
    handle_event_while_active (code);
    return;
  }
  case STATE_waitingForDraw: {
    handle_event_while_waitingForDraw (code);
    return;
  }
  case STATE_unlockedPanel: {
    handle_event_while_unlockedPanel (code);
    return;
  }
  case STATE_waitingForLight: {
    handle_event_while_waitingForLight (code);
    return;
  }
  default: {
    printf("reached a bad spot");
    exit(2);
  }
  }
}

テンプレートエンジンはVelocityを利用する。イベント定義から見ていく。テンプレートのコードは以下のとおり。

#foreach ($e in $helper.events)
#define $helper.eventEnum($e) "$e.code"
#end

velocityのテンプレートコマンドとCプリプロセッサで「#」がカブったがvelocity側にて#defineを無視する。#foreachは繰り返し命令。Embedment Helperを利用し、SwitchHelper?のインスタンスをvelocityコンテキストに置く。

class SwitchHelper...
  private StateMachine machine;

  public SwitchHelper(StateMachine machine) {
    this.machine = machine;
  }
  public Collection<Event> getEvents() {
    return machine.getEvents();
  }

コードの中で参照する定数を作成するのはもう少し作業が必要。

class SwitchHelper...
  public String eventEnum(Event e) {
    return String.format("EVENT_%s", e.getName());
  }

イベントコードそのものを使うこともできるが、読みやすさの観点から定数を生成しました。次は、状態を生成する部分。

#foreach ($s in $helper.states)
#define $helper.stateEnum($s) $helper.stateId($s)
#end
class SwitchHelper...
  public Collection<State> getStates() {
    return machine.getStates();
  }
  public String stateEnum(State s) {
    return String.format("STATE_%s", s.getName());
  }
  public int stateId(State s) {
    List<State> orderedStates = new ArrayList<State>(getStates());
    Collections.sort(orderedStates);
    return orderedStates.indexOf(s);
  }

条件文の生成。

void handle_event(char *code) {
  switch(current_state_id) {
#foreach ($s in $helper.states)
  case $helper.stateEnum($s): {
    handle_event_while_$s.name (code);
    return;
  }
#end
  default: {
    printf("reached a bad spot");
    exit(2);
  }
  }
}

内側の条件の生成。

#foreach ($s in $helper.states)
void handle_event_while_$s.name (char *code) {
#foreach ($t in $helper.getTransitions($s))
  if (0 == strcmp(code, $helper.eventEnum($t.trigger))) {
    current_state_id = $helper.stateEnum($t.target);
#foreach($c in $t.target.commands)
    send_command($helper.commandEnum($c));
#end
  }
#end
}
#end

各状態の遷移を取得する。Semantic Modelで定義された遷移とその他のイベント遷移の両方が必要。

class SwitchHelper...
  public Collection<Transition> getTransitions(State s) {
    Collection<Transition> result = new ArrayList<Transition>();
    result.addAll(s.getTransitions());
    result.addAll(getResetTransitions(s));
    return result;
  }

  private Collection<Transition> getResetTransitions(State s) {
    Collection<Transition> result = new ArrayList<Transition>();
    for (Event e : machine.getResetEvents()) {
      if (!s.hasTransition(e.getCode()))
        result.add(new Transition(s, e, machine.getStart()));
    }
    return result;
  }

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

  • Velocityを知らない人のために、特有のルールについても触れておいた方がいいかと。(getEventsとevents)

担当者のつぶやき

  • テンプレートエンジンは使ったり作ったりしたことある人が多いから、まあ大丈夫な気はする。

みんなの突っ込み