On Thu, Oct 25, 2012 at 11:38 AM, Brian Craft <craft.br...@gmail.com> wrote:

> Multimethods seem like a convenience layer over duck typing: probe the
> object to see what it is, then dispatch. Is that a fair description?
>
>
>
 I think that's pretty accurate.  Others have pointed out that multimethods
can potentially do more than that, but commonly, it's just a way to do
dispatch that leaves things open for extension.

Here's the vanilla way to do dispatch with no special constructs:

(defn circle [radius]
  {:tag :circle, :radius radius})

(defn square [length]
  {:tag :square, :length length})

(defn rectangle [length width]
  {:tag :rectangle, :length length, :width width})

(defn tag-test [tag]
  #(= (:tag %) tag))

(def circle? (tag-test :circle))
(def square? (tag-test :square))
(def rectangle? (tag-test :rectangle))

(defn area-circle [{:keys [radius]}]
  (* 3.14 radius radius))

(defn area-square [{:keys [length]}]
  (* length length))

(defn area-rectangle [{:keys [length width]}]
  (* length width))

(defn area [shape]
  (cond
   (circle? shape) (area-circle shape)
   (square? shape) (area-square shape)
   (rectangle? shape) (area-rectangle shape)))

Here's the way with multimethods:

(defn circle [radius]
  {:tag :circle, :radius radius})

(defn square [length]
  {:tag :square, :length length})

(defn rectangle [length width]
  {:tag :rectangle, :length length, :width width})

(defmulti area :tag)

(defmethod area :circle [{:keys [radius]}]
  (* 3.14 radius radius))

(defmethod area :square [{:keys [length]}]
  (* length length))

(defmethod area :rectangle  [{:keys [length width]}]
  (* length width))

The dispatch function has been eliminated, and we might not need to build
explicit predicates to test for the different types (although in a real
system, you'd probably want those anyway).  Other than that, pretty
similar.  Multimethods become a clearer win if you need inheritance, or if
you need other people to be able to extend the definition of area to new
types without access to your code.

Now let's look at the record version:

(defrecord Circle [radius])
(defrecord Square [length])
(defrecord Rectangle [length width])

(defn area-circle [{:keys [radius]}]
  (* 3.14 radius radius))

(defn area-square [{:keys [length]}]
  (* length length))

(defn area-rectangle [{:keys [length width]}]
  (* length width))

;; Dispatch via protocol

(defprotocol Area
  (area [this]))

(extend Circle
  Area
  {:area area-circle})

(extend Square
  Area
  {:area area-square})

(extend Rectangle
  Area
  {:area area-rectangle})

There are actually several ways to extend protocols to various records.
The most extensible way is to extend records "from the outside" like above,
and to use maps of functions so the functions aren't "locked up" inside the
record but can be reused by other related objects.  But as you can see,
this is fairly verbose and it doesn't give you the kind of speed advantage
that people are generally looking for when they convert to
records/protocols.  So most people end up declaring everything inline, like
this:

(defprotocol Area
  (area [this]))

(defrecord Circle [radius]
  Area
  (area [_] (* 3.14 radius radius)))

(defrecord Square [length]
  Area
  (area [_] (* length length)))

(defrecord Rectangle [length width]
  Area
  (area [_] (* length width)))

I personally haven't had a very good experience with records and protocols,
although it is possible that they have improved since I last did much with
them.  First, I found that records were mostly similar to maps, but behaved
subtly different in ways that bit me occasionally.  Second, the
introduction and repetition of protocol names feel clunky to me, especially
in the common case of one function per protocol.  Third, in order to chase
after the supposed performance gains that records/protocols purportedly
bring, I found myself altering my code in ways that looked less and less
Clojurian, using more dot syntax to access constructors, fields, and
methods, and adding type hints.  Even with all those changes, I seemed to
keep running into places that Clojure was forced to use dynamic reflection
on my record-based code (which is never a problem with maps), and the
reflection more than cancels out any performance gains from the dispatch.

So my vote is for multimethods, which is at least as clean and fast as
manual dispatch with more flexibility.  Records/protocols may have some
additional performance benefits in the right use cases, but in my
experience it definitely comes at a cost.

-- 
You received this message because you are subscribed to the Google
Groups "Clojure" group.
To post to this group, send email to clojure@googlegroups.com
Note that posts from new members are moderated - please be patient with your 
first post.
To unsubscribe from this group, send email to
clojure+unsubscr...@googlegroups.com
For more options, visit this group at
http://groups.google.com/group/clojure?hl=en

Reply via email to