Tuesday, February 28, 2012

On Lisp in Clojure ch 2 (2.4) Polymorphism

This is my second post translating the examples from On Lisp by Paul Graham into Clojure.

Section 2.4 is titled Functions as Properties. What Graham is describing in his example is polymorphism, and there are a couple of ways to do this in Clojure.

First, here is the Clojure version of the method using a conditional statement.

(defn behave [animal]
  (cond
   (= animal 'dog) (do '(wag-tail) '(bark))
   (= animal 'rat) (do '(scurry) '(squeek))
   (= animal 'cat) (do '(rub-legs) '(scratch-carpet))))

My version has a lot of quotes where the original does not, such as '(dog) and '(wag-tail) because I wanted my code to compile, without actually having to create a wag-tail function. Any new animals we create will require editing our behave function.

Rather than go into the details of Graham's alternative method in the book, I will just jump to two ways to do the same thing in Clojure.

Protocols

The first is with protocols. A protocol is a list of functions that can be applied to different data-types. Each function must take at least 1 parameter. It is this required parameter that determines what actually gets called at run time. Protocols are applied to data types that are defined with either deftype or defrecord. For the purposes of protocols, the two are interchangeable, as in my example:

;; define the protocol
(defprotocol animal
  (behave [this] ))

;; define a dog
(defrecord dog [breed]  )

;; add the animal protocol to dog type
(extend dog
  animal
  {:behave (fn [src] (do '(wag-tail) '(bark)))})

;; create a dog
(def my-dog (dog. "collie"))

;; see what it does
(behave my-dog)

;; define a rat
(deftype rat [color])

;; add the animal protocol to the rat type
(extend rat
  animal
  {:behave (fn [src] (do '(scurry) '(squeek)))})

;; create a rat
(def brown-rat (rat. "brown") )

;; see what it does
(behave brown-rat)

Protocols allow you to add polymorphic behaviors to any data type. To get an idea of how powerful they are try typing this in:

(extend String
  animal
  {:behave (fn [src] (do '(what)))})

(behave "huh")

Then consider that java.lang.String is declared as final.

Multimethods

While protocols are similar to overriding functions, multimethods are like overloading. Protocols broadened overriding to make polymorphism work independent of class hierarchies. Multimethods allow overloading based not just on parameter type, but even parameter values.

Here's the multimethod version of the book's example.

;; define the multimethod type
(defmulti behave-multi identity)

;; define implementations for our animals
(defmethod behave-multi 'dog [x]
  (do '(wag-tail) '(bark)))
(defmethod behave-multi 'rat [x]
  do '(scurry) '(squeek))

;; try them out
(behave-multi 'dog)
(behave-multi 'rat)

The following example is pretty dumb, but it will show you that you a way to choose which function to call based on variable values, instead of just variable types in traditional overloading.

(defmulti two-behaviors (fn [num]
                          (if (odd? num)
                            :odd
                            :even)))

(defmethod two-behaviors :odd [num]
  (str num " is odd"))

(defmethod two-behaviors :even [num]
  (str num " is even"))

(two-behaviors 3)
(two-behaviors 4)

This was just a glimpse into protocols and multimethods in Clojure. The Clojure books I have seen devote a chapter to each, and I suspect they still leave a lot unsaid.

No comments:

Post a Comment