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.

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?
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.
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_procinstead of woring about the
lambda...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!
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_procprobably 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)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
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.
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.
And I thought ducktypes were mallard, teal, eider, merganser…