キーワード引数のキーワードをチェックしてみる

fluent interface 流れるようなインターフェイス - #生存戦略 、それは - subtech
Ruby でキーワード引数的な Fluent Interface の実装 - #生存戦略 、それは - subtech
流れるようなインターフェースとメソッドチェーンは違うもの - yvsu pron. yas

最近は流れるようなインターフェースが人気のようだ。

この書き方だと、option がたくさんある時にかっこわるい。

obj.foo(name, :foo => 'bar', :baz => 2)

そっかぁ? けっこう好きなんだけど。
Ruby 1.9だと 「obj.foo(name, foo: 'bar', baz: 2)」 なんて書けるし。

fluent = FluentInterface.fluent WEBrick::HTTPServer, :new
server = fluent.DocumentRoot('/var/www').BindAddress('0.0.0.0').Port(8001).execute

強引っぽくて好きじゃないなあ、俺は。「FluentInterface.fluent」とか「.execute」とかが表に出てるのがやだ。

流れるようなインターフェースのかっこいい例はMochaでしょ、やっぱり。「Time.stubs(:now).returns(Time.local(1993,2,24))」と宣言的にスタブを宣言できるから。なんらかを「宣言」する場合に効果抜群のようだ。

Rubyにおけるモックとスタブについて - http://rubikitch.com/に移転しました
Time.nowのテスト? それMochaでできるよ - http://rubikitch.com/に移転しました
Mochaのstub_everythingは強力 - http://rubikitch.com/に移転しました


で、現状の疑似キーワード引数でtypoによるミスを防ぐのなら、こんな風に書けたらいいのではないか。レッツDSL

class Super
  extend AcceptKeywords

  # :fooを受け付けるmeth1
  accept_keywords :foo
  def meth1(keywords)
  end

  # :hogeを受け付けるmeth2
  accept_keywords :hoge
  def meth2(keywords)
  end

  def meth3(keywords)
  end
end

継承でキーワードを加える場合はこんな感じ。

class Sub < Super
  append_keywords :bar
  def meth1(keywords)
    keywords
  end
end

で、quick hackな実装を。バグあるかも…
Module#method_addedでメソッド追加をフックするのがミソ。追加されたときに、オリジナルのメソッドにキーワード追加機能を加えたメソッドを再定義する。@__keywordsというクラスのインスタンス変数にメソッド名とキーワードのハッシュを格納しておく。

class Module
  attr_accessor :__keywords
end

class KeywordError < StandardError;  end

module AcceptKeywords
  def __accept_keywords(keywords, mode)
    (class << self; self; end).module_eval do
      added = false
      define_method(:method_added) do |name| # !> method redefined; discarding old method_added
        unless added
          added = true
          orig = instance_method name
          if mode == :new
            superclass_keywords = []
          else
            superowner = self.superclass.instance_method(name).owner
            superclass_keywords = superowner.__keywords[name] || []
          end
          (self.__keywords ||= {})[name] = keywords + superclass_keywords
          remove_method name
          define_method(name) do |*args, &block|
            hash = args.last
            if Hash === hash
              extra_keywords = hash.keys - self.class.__keywords[name]
              unless extra_keywords.empty?
                raise KeywordError, "invalid keywords: %p, expects %p" %
                  [ extra_keywords, self.class.__keywords[name] ]
              end
            end
            orig.bind(self).call(*args, &block)
          end
        end
        super name
      end
    end
  end

  def accept_keywords(*keywords)
    __accept_keywords keywords, :new
  end

  def append_keywords(*keywords)
    __accept_keywords keywords, :append
  end
end

class Super
  extend AcceptKeywords
  accept_keywords :foo
  def meth1(keywords)
    keywords
  end
  accept_keywords :hoge
  def meth2(keywords)
    keywords
  end
  def meth3(keywords)
    keywords
  end
end

class Sub < Super
  append_keywords :bar
  def meth1(keywords)
    keywords
  end
end

class SubSub < Sub
  append_keywords :gaz
  def meth2(keywords)
    keywords
  end

  append_keywords :a
  def meth3(keywords)
    super
  end
end



s = Super.new
s.meth1 :foo => 2             # => {:foo=>2}
s.meth1 :bar => 3 rescue $!   # => #<KeywordError: invalid keywords: [:bar], expects [:foo]>
s.meth2 :hoge => 4            # => {:hoge=>4}
s.meth2 :boke => 4 rescue $!  # => #<KeywordError: invalid keywords: [:boke], expects [:hoge]>
s.meth3 :test => 5            # => {:test=>5}
s.meth1 7                     # => 7

t = Sub.new
t.meth1 :foo => 3  rescue $!  # => {:foo=>3}
t.meth1 :bar => 3  rescue $!  # => {:bar=>3}
t.meth1 :aar => 3  rescue $!  # => #<KeywordError: invalid keywords: [:aar], expects [:bar, :foo]>

u = SubSub.new
u.meth2 :goge => 4 rescue $!  # => #<KeywordError: invalid keywords: [:goge], expects [:gaz, :hoge]>
u.meth3 :a => 77              # => {:a=>77}

コメント大歓迎。