There’s more than one way to do that

Clojure has a simple way of defining multi-methods. If you’ve seen multi-methods before in Common Lisp, these are a very simplified version, without the ability to call next-method, without any kind of topological sorting of applicable methods being used to find the most specific and without the notion of an effective method. As an aside, the definitive book on the design of Clos, The Art of the Metaobject Protocol, is one of the best computing book I’ve read, taking the reader through the Common Lisp object system and explaining the design decisions that were required along the way.

Clojure’s system for calling multimethods certainly isn’t as beautiful as Clos, but it is quite neat and simple, and we’ll take a look at it here.

In order to work out which method is closest to a set of arguments, we need the notion of a hierarchy which is realised in an isA function together with the notion of one type dominating another. In the implementation in MultiFn

private boolean isA(Object x, Object y) throws Exception{
    return RT.booleanCast(isa.invoke(hierarchy.deref(), x, y));
}

private boolean dominates(Object x, Object y) throws Exception{
    return prefers(x, y) || isA(x, y);
}

where prefers is defined as

private boolean prefers(Object x, Object y) throws Exception{
    IPersistentSet xprefs = (IPersistentSet) getPreferTable().valAt(x);
    if(xprefs != null && xprefs.contains(y))
        return true;
    for(ISeq ps = RT.seq(parents.invoke(y)); ps != null; ps = ps.next())
        {
        if(prefers(x, ps.first()))
            return true;
        }
    for(ISeq ps = RT.seq(parents.invoke(x)); ps != null; ps = ps.next())
        {
        if(prefers(ps.first(), y))
            return true;
        }
    return false;
}

This code uses the clojure builtin parents function, for checking the relationship between things. The prefer table is a table that can be set using the functions prefer-method, and accessed using prefers, and is used for breaking ties when there are multiple methods that are equally related to the given item. We have an example of this at the end of this post.

We can access the global hierarchy using the parents and ancestors functions.

foo=> (parents (class a))
#{java.lang.Object}
foo=> (ancestors (class a))
#{java.lang.Object}
foo=> (parents (class b))
#{clive.test.A}
foo=> (ancestors (class b))
#{java.lang.Object clive.test.A}

The isa function defaults to the Clojure isa? function. This has the following comment in core.clj

(defn isa?
  "Returns true if (= child parent), or child is directly or indirectly derived from
  parent, either via a Java type inheritance relationship or a
  relationship established via derive. h must be a hierarchy obtained
  from make-hierarchy, if not supplied defaults to the global
  hierarchy"
([child parent] (isa? global-hierarchy child parent))
([h child parent] …)

There are a series of functions including parents, ancestors, descendants that use a hierarchy to relate various objects, with the user being allowed to define extra relationships using derive and underive.

MultiFn itself tries to cache a lot of the calls to these relationship functions to speed up subsequent calls. It also caches the results of its search to find the particular method that matches a set of arguments.

In the following, we will assume the following Java class definitions have been compiled and are placed on the class path.

package clive.test;
public class A {}
public class B extends A {}

We can import these classes into the current session.

user=> (import ‘clive.test.A)
nil
user=> (import ‘clive.test.B)
nil

And then define instances of these classes.

user=> (def a (clive.test.A.))
#’user/a
user=> (def b (clive.test.B.))
#’user/b

We can do some simple tests on the isa? function.

user=> (isa? (class a) clive.test.A)
true
user=> (isa? (class a) clive.test.B)
false
user=> (isa? (class b) clive.test.A)
true
user=> (isa? (class b) clive.test.B)
true

We can use defmulti to define new function. We have to supply a function that is executed on the arguments in order to get the values that are used to determine the applicable method. In this case, we’ll use the class function to convert the arguments into suitable values. The defmulti function can take arguments giving the equivalent of an isa? function for defining the hierarchy, and a value that is used for a default method. These default to the values isa? and :default respectively.

user=>  (defmulti foo class)
#’user/foo
user=> (defmethod foo clive.test.A [x] (println "called on A" x))
#<MultiFn clojure.lang.MultiFn@476128>
user=> (foo a)
called on A #<A clive.test.A@5fcf29>
nil
user=> (foo b)
called on A #<B clive.test.B@125844f>
nil

We didn’t define a method that is applicable to either the java.lang.Integer class or the :default, so calling the multi function with 10 gives an error.

user=> (foo 10)
java.lang.IllegalArgumentException: No method in multimethod ‘foo’ for dispatch
value: class java.lang.Integer (NO_SOURCE_FILE:0)

If we also define a method on B, then the most applicable method will be used, as determined by the dominates method above.

user=> (defmethod foo clive.test.B [x] (println "called on B"))
#<MultiFn clojure.lang.MultiFn@476128>
user=> (foo a)
called on A #<A clive.test.A@5fcf29>
nil
user=> (foo b)
called on B
nil
user=> (foo 10)
java.lang.IllegalArgumentException: No method in multimethod ‘foo’ for dispatch
value: class java.lang.Integer (NO_SOURCE_FILE:0)

We can define a default method.

user=> (defmethod foo :default [x] (println "called on default"))
#<MultiFn clojure.lang.MultiFn@476128>
user=> (foo a)
called on A #<A clive.test.A@5fcf29>
nil
user=> (foo b)
called on B
nil
user=> (foo 10)
called on default
nil

We can remove methods. If we remove the method on A, then calling foo with instances of A will lead to the default method being called.

user=> (remove-method foo clive.test.A)
#<MultiFn clojure.lang.MultiFn@476128>
user=> (foo a)
called on default
nil
user=> (foo b)
called on B
nil
user=> (foo 10)
called on default
nil

We can also determine which method is going to be called. This is the return value of the get-method function and we can then apply this to some arguments. When the method is applied, no additional checking is performed, as we can see by getting a method and then applying it to a completely different set of arguments.

user=> (get-method foo (class b))
#<user$fn__146 user$fn__146@15a6029>
user=> (apply *1 [b])
called on B
nil
user=> (apply *2 [20])
called on B
nil

The isa? function also works on vectors of elements, allowing specialisation on multiple arguments.

foo=> (isa? [(class a)(class b)] [(class a)(class a)])
true

foo=> (defmulti foo2 (fn [x y] [(class x)(class y)]))
#’foo/foo2
foo=> (defmethod foo2 [clive.test.A clive.test.A] [x y] (println "both A"))
#<MultiFn clojure.lang.MultiFn@13f210f>
foo=> (defmethod foo2 [clive.test.B clive.test.B] [x y] (println "both B"))
#<MultiFn clojure.lang.MultiFn@13f210f>
foo=> (foo2 a a)
both A
nil
foo=> (foo2 a b)
both A
nil
foo=> (foo2 b a)
both A
nil
foo=> (foo2 b b)
both B
nil

We can quickly demonstrate the prefer behaviour by extending our example classes

public interface C {}
public class D extends A implements C {}

user=> (import ‘(clive.test C))
nil
user=> (import ‘(clive.test D))
nil

We can now define methods on the class A and the interface C.

user=> (defmulti foo3 class)
#’user/foo3
user=> (defmethod foo3 clive.test.A [x] (println "A"))
#<MultiFn clojure.lang.MultiFn@13785d3>
user=> (defmethod foo3 clive.test.C [x] (println "C"))
#<MultiFn clojure.lang.MultiFn@13785d3>
user=> (def d (clive.test.D.))
#’user/d

A call on an instance of D is ambiguous – one method is on the interface and one on the class so there is no ordering.

user=> (foo3 d)
java.lang.IllegalArgumentException: Multiple methods in multimethod ‘foo3’ match dispatch value: class clive.test.D -> interface clive.test.C and class clive.test.A, and neither is preferred (NO_SOURCE_FILE:0)

We can set one to be preferred over the other.

user=> (prefer-method foo3 clive.test.A clive.test.C)
#<MultiFn clojure.lang.MultiFn@13785d3>
user=> (foo3 d)
A
nil

And can check to see what preferences have been set so far.

user=> (prefers foo3)
{clive.test.A #{clive.test.C}}

This is a very concise way of avoiding a function with a lot of conditions for selecting a particular case, though the construct doesn’t appear to be used very often in Clojure.

Advertisements
This entry was posted in Computers and Internet. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s