In Java / C++ you'd typically approach this with the visitor pattern, with something like the following.
interface Foo {
// ... lots of exciting methods
void accept( FooVisitor visitor );
}
interface FooVisitor {
void visit( Bar bar );
void visit( Baz baz );
}
class Bar implements Foo {
//... implementation
public void accept ( FooVisitor visitor ) {
visitor.visit( this );
}
}
class Baz implements Foo {
//... implementation
public void accept( FooVisitor visitor ) {
visitor.visit( this );
}
}
Why's this not good? Well, the
FooVisitor
has to know about all the subclasses, which is pretty nasty. In addition, well, it's just a lot of typing - way too much scaffolding. Languages like Smalltalk and Lisp both support multi-methods natively which give you all the benefits of this "pattern", without the crazy verbosity of the Java version.Clojure multimethods support dispatching on variables other than the runtime type of an argument. From the Clojure docs, you can dispatch on "types, values, attributes and metadata of, and relationships between, one or more arguments".
Clojure multi-methods consist of two things:
* Some methods (the multi!)
* A dispatcher function (chooses which one)
Here's a trivial example defining an increment function that dispatches on the class of the arguments.
class
is a built in function within Clojure core that gives the class of the object.
(defmulti my-increment class)
(defmethod my-increment String [s]
(let [x (Integer/valueOf s)]
(str (my-increment x))))
(defmethod my-increment Integer [i]
(+ i 1))
user> (my-increment 4) ==> 5
user> (my-increment "4") ==> "5"
But, we do not just have to be boring and dispatch only on the type of the argument, we could be dispatching on the value:
(defmulti my-decrement identity) ;; identify is built in (fn [x] x)
(defmethod my-decrement 0 [x]
99999)
(defmethod my-decrement :default [x]
(- x 1))
user> (my-decrement 2) ==> 1
user> (my-decrement 1) ==> 0
user> (my-decrement 0) ==> 99999
The dispatching function isn't limited to just the first argument, you could dispatch on the type of multiple arguments e.g.
(defmulti my-add (fn [x y] (and (string? x) (string? y))))
(defmethod my-add true [x y]
(str x y))
(defmethod my-add false [x y]
(+ x y))
user> (my-add 3 4) ==> 7
user> (my-add "3" "4") ==> "34"
This gives about the most general method of method dispatch imaginable!
The documentation explains how this goes further still, allowing relationships to be created with
derive
and how you can add/remove/define an ordering implementations of methods at runtime.Reading Programming Clojure shows that multi-methods are very rarely used in most of the current Clojure code (only about 1 occurrence per thousand lines of code). There's some guidelines in the book about choosing whether to use multi-methods, but it's summed up best with "try writing the function in both styles, and pick the one that seems more reliable".
I had somehow missed "derive" in the clojure documentation, thanks for pointing that one out!
ReplyDelete