Specifying Duck Types in Ruby

Posted by Jeremy Voorhis Thu, 03 May 2007 22:59:00 GMT

While prototyping a RESTful web service, I wrote the following code to allow Rails to parse a request’s parameters from a POST request containing JSON.


ActionController::Base.param_parsers[Mime::JSON] = JSON.method(:parse)

Unfortunately, it did not work, while the following code did:


ActionController::Base.param_parsers[Mime::JSON] = lambda { |data| JSON.parse(data) }

Theoretically, both of these should accomplish the same thing: assign an object that responds to the message call with an arity of 1 to Mime::JSON entry of the param_parsers hash. They lead to different results, however, because a case statement within the Rails HTTP request parsing code watches for an instance of Proc, instead of an object which responds to call.

As a proof of concept, I whipped up this DuckType class:


class DuckType
  def initialize(*args)
    @args = args
  end

  def ===(other)
    @args.all? { |arg| other.respond_to?(arg) }
  end
end
Now, we can replace this statement from cgi_methods.rb

case strategy = ActionController::Base.param_parsers[mime_type]
  when Proc
    strategy.call(raw_post_data)
  # snip...
end
with the following:

Callable = DuckType.new :call
case strategy = ActionController::Base.param_parsers[mime_type]
  when Callable
    strategy.call(raw_post_data)
  # snip...
end

As you can see, an instance of DuckType has a case comparison operator which asserts that all specified messages are supported, allowing you to focus on an object’s capabilities rather than its class.

My DuckType implementation in its current form is rather crude, but if enough people found this technique useful, I would consider properly packaging and extending it for general use.

Comments

  1. josh said about 2 hours later:

    Nice. What you call duck types used to be called protocols way back when. This looks like a handy way to compare object behaviors.

    It might be nice to have a place to put that Callable protocol so it can be shared among interested parties. Of course, that way lies the madness of type registries, but the other option is not very DRY. Then you have to contend with evolution of protocols over time and brittle types captured in the registry. Can I have a beer now?

  2. JV said about 2 hours later:

    Josh,

    In what context is the term “protocol” equivalent? Could you point me to some background information?

    Because my duck types are ordinary Ruby code, a collection of duck types could be distributed as plain Ruby code, _require_d rather than registered.

    The goal is to shift our reasoning about types in Ruby to emphasize capability rather than class. This is similar to interfaces in Java, except that an object may satisfy a duck type with no knowledge of the type, as opposed to Java classes, where the object must derive from a class which explicitly implements the interface.

    You can have a beer now.

  3. Kenny Parnell said about 5 hours later:

    Jeremy,

    ActionController::Base.param_parsers[Mime::JSON] = JSON.method(:parse)

    should not work the same as

    ActionController::Base.param_parsers[Mime::JSON] = lambda { |data| JSON.parse(data) }

    because JSON.method(:parse) returns a method and methods do not respond to call.

    You could have done

    ActionController::Base.param_parsers[Mime::JSON] = JSON.method(:parse).to_proc

    instead of woring about the lambda...

  4. JV said about 5 hours later:

    Methods do respond to call, and in many cases, can be treated like a closure object.

    I did, however, forget about the Method#to_proc. Good call!

  5. Kenny Parnell said about 6 hours later:

    I just read what I wrote and realized that I wan’t clear.

    JSON.method(:parse)

    returns a method when that method is passed as a block to another method in a sense it is converted to the proc:

    Proc.new { JSON.parse(nil) }

    which does not accept an argument when it is called and passes nil as ‘data’

    on the other hand

    lambda {|data| JSON.parse(data)}

    does accept an argument.

    P.S. JSON.method(:parse).to_proc probably wouldn’t work either for the same reason. :-) I wasn’t thinking before.

    For example

    class MyClass def parse puts "hi from MyClass#parse" end end pr = Proc.new { puts "hi from anonymous Proc" } methproc = MyClass.new.method(:parse).to_proc [1,2,3].each &pr # "hi from anonymous Proc\n" * 3 [1,2,3].each &methproc # in `talk': wrong # of arguments(1 for 0)
  6. JV said about 6 hours later:

    That is incorrect, unless we are using wildly different versions of Ruby. Method objects obtained via Object#method, will retain their arity.

    Your example ought to run just fine without displaying the aforementioned error on Ruby 1.8.x

  7. Dan said about 20 hours later:

    I’d love for something like this to be packaged up. I’d even go so far as to suggest bundling it up into an ActiveSupport module, and replacing all the when klass, kind_of? and is_a? type calls throughout the Rails source.

  8. Thomas said about 23 hours later:

    Jeremy,

    I think the concept of protocol Josh was referring to is outlined pretty decently here. Though, I do seem to recall a variation on this that precedes OOP. I can see how your approach does bare a good resemblance to that definition.

  9. aMOMynous said 4 days later:

    And I thought ducktypes were mallard, teal, eider, merganser…

(leave url/email »)