DSL / Dynamic Reception


DSL

ダイナミックレセプション

一言要約

  • 「ダイナミックレセプション」とは、動的にメソッドを生成する技術。
  • たとえば、Ruby on Rails の Active Record で使用されている。

要約1

  • 「ダイナミックレセプション」とは、動的にメソッドを生成する技術。
    • 静的言語の場合、定義されていないメソッドを呼び出そうとすると、コンパイル時にエラーになる。
    • 動的言語の場合、通常、実行時にエラーとなるが、ダイナミックレセプションでその振る舞いを調整し、特定の動きをするように出来る。

仕組み

  • 未定義メソッドを呼び出そうとすると、Ruby の場合 method_missing というメソッドが呼びされるが、それをオーバーライドすることで、その振る舞いを変更できる。
  • ダイナミックレセプションを使った良い例のひとつは、自動デリゲートである。
  • パラメータの名前を示すものを、メソッド名に埋め込むのも良いやり方。(下記、例)
    find_by_firstname("martin")
    find_by_firstname_and_lastname("martin", "fowler")
  • もうひとつは、Method Chaining と Expression Builder を組み合わせるやり方。(下記、例)
    find_by.firstname("martin").and.lastname("fowler)
    find_by.firstname.martin.and.lastname.fowler
  • 引用符にくるまないようにすると、雑音が減る。

使用するとき

  • パラメータをメソッド名に含めると、いくつかの点で魅力的。
    • 少ない努力でできる。
    • find_by_firstname_and_lastname メソッドが、person クラスにあるのは妥当。
    • 多くの組み合わせがあるなら、かなりの時間節約ができる。
    • 他の方法(下記)もあるが、流暢ではない。
      キーワードとパラメータ find(:firstname, "martin", :lastname, "fowler")
      クロージャ find {|p| p.firstname == "martin" and p.lastname = "fowler"}
      文字列中に外部DSL find("firstname == martin lastname == fowler")
  • パラメータにメソッド名を入れるのは、句読点により一貫性を付けられる。
    find.by.firstname.martin.and.lastname.fowler
  • でも、フィールド名とパラメタとしてのデータを置く方法が好き。
    find_by.firstname("martin").and.lastname("fowler")
  • これらのことが、特別なケースなしで一般化できるのはダイナミックレセプションだけ。
  • ダイナミックメソッドから他の目的に必要であるメソッドまで明確に変換できるときだけ価値がある。
  • ダイナミックレセプションの特定のケースを扱うための特別なメソッドを書く必要があるなら、他の方法を使う必要がある。
  • 問題点
    • 静的な言語では、ダイナミックレセプションは使えない。
    • 動的な言語でも、注意深くプログラミングしないと、とんでもないデバッグトラブルに見舞われる。スタックトレースは何も受け付けなくなるかも。
    • 多くの動的言語は2といった数字を受け付けない。
      find_by.age.greater_than.2
    • 複雑な論理式もうまく表現できない。
      find_by.firstname.like.("m*").and.age.greater_than(40).and.not.employer.like("thought*")
      • ただし、単純な事例ではダイナミックレセプションは使うべき。
      • Active Recordは、より複雑な式はわざとサポートしていない。他の方法を使うべき。
      • 違う複雑さのものに、違う実装を提供するという解決策は良いと思う。(他の人たちは、ひとつの解決策を望むかも知れないが。)

メソッド名のパーズを使用したプロモーションポイント (Ruby)

  • 旅程を旅行するためのポイントを割り当てるスキームを考えてみる。
  • フライト、ホテルの滞在、レンタカーなどの項目から成る。
  • 人々が頻繁な旅行ポイントを得るのをフレキシブルな方法でできるようにしたい。
  • たとえば、ボストンからのフライトに300ポイントを得るとしたら。
    @builder = PromotionBuilder.new
    @builder.score(300).when_from("BOS")
  • 特定の空港から飛んで特定のホテルのブランドに滞在することにより得点する。
    @builder = PromotionBuilder.new
    @builder.score(350).when_from("BOS")
    @builder.score(100).when_brand("hyatt")
  • 特定のエアラインでボストンから出発することで得点する複合した飛行ルール。
    @builder = PromotionBuilder.new
    @builder.score(140).when_from_and_airline("BOS","NW")
  • このアプローチは、当然、Active Recordのdynamic finderに似ている。
  • もし、興味があれば、Jamis Buck の説明を見てほしい。

要約2

  • 例:プロモーションポイントのチェーン版(Ruby)
    • 上記の例と同じものをメソッドのチェーンを用いて実現する。
    • コードイメージは以下の通り
      @builder.score(300).when.from.equals.BOS
  • モデルは上記の例と同様
  • 違いはビルダに現れる
    • 式の要素を名前、演算子、条件に分けた上で、各部分を細かく解析する。
    • 値をメソッドとして記述するので、ダイナミックレセプションを使う。
      class ConditionAtributeNameBuilder...
        def method_missing method_id, *args
          @content = method_id.to_s
          return ConditionOperatorBuilder.new(@parent)
        end
  • 例:秘密のパネルコントローラから「:」を取り除く(JRuby)
    • オリジナル(抜粋)
      event :doorClosed, "D1CL"
      event :drawOpened,  "D2OP"
      event :lightOn, "L1ON"
      event :doorOpened,  "D1OP"
      event :panelClosed, "PNCL"
  • 取り除いた版(抜粋)
    events do
      doorClosed  "D1CL"
      drawOpened  "D2OP"
      lightOn     "L1ON"
      doorOpened  "D1OP"
      panelClosed "PNCL"
    end
  • イベントの内容をブロックとして受け取り、各イベント名をメソッドとしてダイナミックレセプションで評価する。
    class EventBuilder < Builder
      def method_missing name, *args
        @parent.add_event(name, args[0])
      end
    end

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

  • Typo : dynamically ⇒ dynmacially, dynmically

担当者のつぶやき

  • 要は、DSLにおける可変部分をメソッドとして記述し、存在しないメソッドに対するふるまいをオーバーライドすることでその値を評価すると言う仕組み。「さすがに悪ノリでは?」という気がしてなりません。動的言語はホントになんでもありですね・・・。(わ)
  • 自分の中では自然言語を読む脳とプログラム言語を読む脳は違うような気がしています。プログラム言語を自然言語に近づけ過ぎると逆に何をやっているか分からない。関数型言語に詳しくなると、もう少し考え方が変わるんでしょうか?(わ)

みんなの突っ込み