Robert D. wrote:
On Fri, Nov 28, 2008 at 10:54 PM, Brian A. [email protected] wrote:
[Message Passing vs. Multimethods]
Sure agree we do not have to prove that Ruby is powerful enough
and
the link you were presenting is a little confusing to me.
Here’s my little attempt at explaining multimethods / multiple
dispatch.
In Ruby and most other single-dispatch OOP languages, methods “belong”
to objects, or, in the case of class-based OO languages like Ruby, to
classes of which the objects are instances.
So, we generally send a message like this:
receiver.message(param, anotherparam)
However, we could just as well use a procedure instead of a method and
call it like this:
message(receiver, param, anotherparam)
All the information is still there, the method body has access to all
the same parameters as it had before. The only difference is that in
the method example, the receiver parameter was passed implicitly into
the method body and was magically accessible via the special variable
keyword “self”, where in the procedure example it was passed
explicitly.
There is one important difference however, and it is one of the
pillars of OO: polymorphism.
The way that procedures are usually implemented, there can only ever
be one procedure with a specific name. With methods however, there can
be many methods with the same name, and which method to execute gets
determined at runtime based on the type (or class) of the special
receiver parameter. This is polymorphism, or, more precisely
single-dispatch runtime subtype polymorphism.
This is where multimethods come in.
A multimethod is a procedure like above. But there can be many
procedures with the same name! And which one to execute is
determined at runtime, not only by looking at the types of the first
parameter (like in single-dispatch OO), but by looking at the types of
all parameters! In that sense, multimethods are more powerful than
methods: you can write a multimethod which is only polymorphic in its
first parameter, which is exactly the same as with a method. But you
can also write a multimethod which is polymorphic in more of its
parameters, which you cannot do with methods. You can fake it,
though: the Visitor pattern is basically an implementation of
double-dispatch (i.e. polymorphism on two parameters instead of one)
for single-dispatch languages. Ruby’s coercion protocol is another.
Here is a simple example how that might work in Ruby. Remember the
coercion example in the Pickaxe book with the Roman numerals?
Basically, the problem is this: when I call
'IV'.to_roman + 3
that works, but
3 + 'IV'.to_roman
doesn’t. Why? Because my Roman class knows about Fixnums but Ruby’s
builtin Fixnum class doesn’t know about Roman numerals. To deal with
this, Ruby has this coercion protocol. In this case, Fixnum#+ calls
‘IV’.to_roman.coerce(3), which in turn returns something like [3, 4]
which can then be used to retry the original operation. With
multimethods, all of this would be unnecessary. The “+” method would
not be defined inside of Fixnum and Roman (and Float and String and
Array and …) but instead as a multimethod:
# This would be part of Ruby Core:
defmulti +(a, b)
def +(Fixnum f1, Fixnum f2)
# do stuff
end
# This would be part of your library:
def +(Roman r, Numeric n)
r.to_int + n
end
def +(Numeric n, Roman r)
n + r.to_int
end
Basically, what multimethods do, is to free polymorphism from the
tyranny of the receiver.
We can go one step further, however: in CLOS multimethods can not only
dispatch on the type of all of their arguments, but also on the
value of all of their arguments. This enables something like this
classic example:
defmulti factorial(n)
def factorial(Integer n)
return n * factorial(n - 1)
end
def factorial(0)
return 1
end
In all of these examples, the dispatch function became more and more
powerful: in procedures, there is no dispatch function. In methods,
the dispatch function is basically a simple switch on the type of the
first parameter (although not that simple: it has to follow the
inheritance hierarchy, of course). In simple multimethods, the
dispatch function switches on the types of all arguments. In CLOS,
the dispatch function switches on the types and values of all
arguments.
However, in all of these cases, the dispatch function is a fixed part
of the language implementation. So, Clojure takes the obvious next
step: when declaring a multimethod, you can provide your own
dispatch function and you can do whatever you want with the arguments.
Switch on the first, switch on all, switch on the sum of the 7th and
42nd, switch on the phase of the moon …
In Ruby, that would look something like this (stolen from the Clojure
website):
defmulti encounter(animal1, animal2)
return animal1.class, animal2.class
end
def encounter(Lion, Rabbit)(l, r)
eat(l, r)
end
def encounter(Lion, Lion)(l1, l2)
fight(l1, l2)
end
def encounter(Rabbit, Rabbit)(r1, r2)
mate(r1, r2)
end
def encounter(Rabbit, Lion)(r, l)
run(r)
end
And here the ubiquitous enterprise joke:
defmulti enterprise_hello_world()
return rand <= 0.9, rand <= 0.5
end
def enterprise_hello_world(true, true)()
puts 'Hello, World!'
end
def enterprise_hello_world(true, false)()
sleep 10
puts 'Hello, World!'
end
def enterprise_hello_world(false, *)()
raise RuntimeError
end
Note the method body of defmulti: this is not the code for the
method, this is the dispatch function. Its return value will be
matched against the pattern definition in the first set of
parantheses. The method definitions have two sets of parameters:
first the pattern that is matched against the dispatch function, then
the formal parameters of the method.
Whereas simple multimethods free polymorphism from the tyranny of the
first argument, this frees it from the tyranny of the language
designer.
Anyway, that’s my little explanation of multimethods, from someone who
understands them as just as badly as you do (-:
It’s gotten a little long (note to self: start a blog, damnit!), I
hope it’s not too long; and I hope it’s helpful.
Cheers,
jwm