This article is part of a series describing a port of the samples from Practical Common Lisp (PCL) to Clojure. You will probably want to read the intro first.
This article covers Chapter 16, Object Reorientation: Generic Functions.
In Common Lisp, a generic function defines an abstract operation and a parameter list. In Clojure, a multimethod takes a similar role:
(defmulti draw :shape)
The multimethod's name is multi
, and :shape
is a dispatch function used to select the actual concrete implementation. (Remember that keywords like :shape
are also lookup functions.) Now, I can create one or more methods:
(defmethod draw :square [shape] "TBD: draw a sqaure")
(defmethod draw :circle [shape] "TBD: draw a circle")
The first method will draw things with a :shape
of :square
, and the second method will draw things with a :shape
of :circle
:
user=> (draw {:shape :square, :length 10})
"TBD: draw a square"
user=> (draw {:shape :circle, :radius 8})
"TBD: draw a circle"
The draw
multimethod is emulating single inheritance, if you think of an object's :shape
value as its type. But the multimethod mechanism is more general.
Let's say that I need to implement account withdrawals. Different kinds of accounts will have different rules:
The multimethod for withdraw
could look like this:
(defmulti withdraw :account-type)
The bank account implementation will do a simple withdraw.
(defmethod withdraw :bank [account amount]
(raw-withdraw account amount))
PCL uses Common Lisp's method combination to share implementation code between the different account types. Clojure's dispatch is much more general, so a general method combination mechanism is not appropriate. I am taking a different approach, pulling the shared code into a helper function raw-withdraw
:
(defn raw-withdraw [account amount]
(when (< (:balance account) amount)
(throw (IllegalArgumentException. "Account overdrawn")))
(assoc account :balance (- (:balance account) amount)))
The withdrawal differs from the original PCL implementation in one other way. The original code mutated the account
. Since mutation is a no-no, I am instead returning a new account object, assoc
ing in the changed balance. In the example below, I am using a let
just to show that the original account is unchanged.
(let [original-state {:account-type :bank :balance 100}
updated-state (withdraw original-state 50)]
(println original-state updated-state))
{:balance 100, :account-type :bank} {:balance 50, :account-type :bank}
The checking account is a little more complex. First, I have to shuttle money in from the overdraft account (if necessary), then raw-withdraw
as before:
(defmethod withdraw :checking [account amount]
(let [over-account (account :overdraft-account)
over-amount (- amount (:balance account))
withdrawal-account
(if (> over-amount 0)
(merge account
{:overdraft-account (withdraw over-account over-amount)
:balance amount})
account)]
(raw-withdraw withdrawal-account amount)))
Again, all the objects are immutable. The merge
function returns a new account object (possibly with an overdraft), and the raw-withdraw
returns another object:
(let [overdraft {:account-type :checking, :balance 1000}
original-state {:account-type :checking
:balance 100
:overdraft-account overdraft}
updated-state (withdraw original-state 500)]
(println original-state)
(println updated-state))
{:overdraft-account {:balance 1000, :account-type :checking},
:balance 100,
:account-type :checking}
{:overdraft-account {:balance 600, :account-type :checking},
:balance 0,
:account-type :checking}
In languages like Java, methods are polymorphic on their first (implicit) parameter. Because multimethods dispatch on arbitrary functions, they can be polymorphic on all of their parameters.
For example, a music library might implement a beat
method that is polymorphic on both the drum
and the stick
:
(defmulti beat (fn [d s] [(:drum d)(:stick s)]))
(defmethod beat [:snare-drum :brush] [drum stick] "snare drum and brush")
(defmethod beat [:snare-drum :soft-mallet] [drum stick] "snare drum and soft mallet")
The first beat
method matches only snare drum + brush, etc.:
user=> (beat {:drum :snare-drum} {:stick :brush})
"snare drum and brush"
user=> (beat {:drum :snare-drum} {:stick :soft-mallet})
"snare drum and soft mallet"
If no methods match the dispatch value, Clojure throws an exception:
user=> (beat {:drum :bongo} {:stick :none})
java.lang.IllegalArgumentException: No method for dispatch value
... stack trace elided ...
Or, you can define a :default
that will match if no other dispatch value matches:
(defmethod beat :default [drum stick] "default value, if you want one")
user=> (beat {:drum :bongo} {:stick :none})
"default value, if you want one"
The PCL chapter demonstrates dispatch based on one or more arguments to a function, and those examples are duplicated above. There are many other things you might do with defmulti
, but since they are not covered in PCL I will declare them out of scope here, and point you to some other reading: