Tuesday, November 8, 2011

Dynamic dispatch is not enough to replace the Visitor Pattern

I've been bugged a little by how poorly understood the Visitor pattern is by people claiming it's not needed in their particular language because of some funky feature of the language. In the Clojure world dynamic dispatch or multi-methods are the supposed giant killer, (and I'm an Lisp fan of old, and Clojure is fast growing on me, this will be a rant about the fans who rail against an enemy and get it wrong).

The Visitor pattern has two parts, an Iterator and Dispatcher.

The mistake is that because Clojure has dynamic dispatch then it must be the dispatcher that gets replaced. In fact the dynamic dispatch really can help with the iterator, allowing different things in the structure to be treated differently. But as the example below shows it's not sufficient for some even more arbitrary dispatch types, not without adding extra information to parameters to facilitate the dispatch, which is an unnecessary added complexity; unnecessary added complexity being my basic definition of bad design.

My favourite example is a tree traversal engine, for something like an AST, with data like this: (def tree '(:a (:b :c :d) :e))

Now I want to do arbitrary things to this tree, for example, render it as JSON data:

{"fork": ":a",
 "left": {
   "fork": ":b",
   "left": ":c",
   "right": ":d"},
 "right": ":e"}

And also as an fully bracketed infix language: ((:c :b :d) :a :e)

There is absolutely nothing, but nothing, neither in the data nor the traversal that can be used to distinguish between rendering as JSON data or rendering for an infix language. This not a candidate for any sort of algorithmic dynamic dispatch based on any part of the data: the distinction is arbitrary based on functions chosen without regard to the data.

It's quite fair to note that there are two different types of nodes, branching and terminal, that need to be traversed in different ways, which can be usefully used for dynamic dispatch, but in that's dynamic dispatch in the traversal engine not the final functional/Visitor dispatch.

First define a generic traversal:

(declare ^:dynamic prefix 
         ^:dynamic infix 
         ^:dynamic postfix 
         ^:dynamic terminal)

(defmulti traverse #(if (and (seq? %) (= 3 (count %))) :branching :leaf))

(defmethod traverse :branching [[fork left right]] 
  (do
    (prefix fork)
    (traverse left)
    (infix fork)
    (traverse right)
    (postfix fork)))

(defmethod traverse :leaf [leaf]
  (terminal leaf))

Nice application of multi-methods and dynamic dispatch. Note that in the most recent versions of Clojure (1.3.0) dispatch defaults to a slightly faster common case of non-dynamic dispatch, so you have to declare the dynamic binding type. If you know you need dynamic dispatch you need to tinker with meta-data, and you can do that to other people's code from outside: (alter-meta! #'their-symbol assoc :dynamic true)

For ease sake I'm also going to provide a default set of behaviour, doing nothing. Notice the matching dynamic binding type specified as meta-data:

(defn ^:dynamic prefix   [fork] nil)
(defn ^:dynamic infix    [fork] nil)
(defn ^:dynamic postfix  [fork] nil)
(defn ^:dynamic terminal [leaf] nil)

Now to get it to do something real, rendering in a fully bracketed in infix style. Notice that binding doesn't need explicit dynamic meta-data, because dynamic is the heart of binding:

(binding [prefix   (fn [_] (print "("))
          infix    (fn [fork] (print " " fork " "))
          postfix  (fn [_] (print ")"))
          terminal (fn [leaf] (print leaf))]
  (traverse '(:a (:b :c :d) :e)))

How about printing the symbols space separated in reverse polish notation, as if for a stack language like PostScript or Forth. Notice how I only need to provide two of the four possible bindings thanks to the default handlers.

(binding [postfix  #(print % " ")
          terminal #(print % " ")]
  (traverse '(:a (:b :c :d) :e)))

The fully bracketed rendering illustrates one other very naive mistake with many peoples understanding of the Visitor pattern. Some nodes are the data to more than one function call during the traversal, the distinction being dependent on the traversal pattern and the nodes location in the structure. The final dispatch to the is not determined by the data of the node in isolation, the individual node data being passed to the function is insufficient to choose the function.

There are some useful applications of multi-methods in the final dispatch, for instance to distinguish between different types of terminals as in this example:

(defmulti funky-terminal #(if (number? %) :number (if (string? %) :string :other)))
(defmethod funky-terminal :other [terminal] (println "other: " terminal))
(defmethod funky-terminal :number [terminal] (println "number: " terminal))
(defmethod funky-terminal :string [terminal] (println "string: " terminal))
(binding [terminal funky-terminal]
  (traverse '(:a (:b 42 :x) "one")))

But that dispatch isn't the whole of the Visitor pattern, and frankly to hold it up as the Visitor pattern only demonstrates that you haven't grokked the complexity of dispatch from a complex data structure.

Consider another typical use of the Visitor pattern: traversal of graphs. Let's propose a traversal of a graph that is is guaranteed to reach all nodes and to traverse all edges while following edges through the graph. There are many graphs where this would require visiting some nodes and some edges multiple times. But there are many applications that would only want to give a special meaning to the first traversal of an edge or the first reaching of a node. There may even need to indicate the traversal has restarted discontinuously in a new part graph in those cases were there is not edge leading back from a node or where the graph is made of disconnected sub-graphs. Nothing in the node itself can tell you that this is the first or a subsequent reaching or traversal, much less that the traversal has had to become discontinuous with regard to following edges, (maybe you could infer that from reaching a node without passing an edge, maybe you could track nodes and edges, but why do all that work when a good visitor will tell you directly by calling a suitably named functions; if we're going to enjoy functional programming, lets not be ashamed to use functions in simple direct ways). All these different function calls represent different targets of dynamic binding in the model I've presented her. Of course you could use multi-methods to achieve dynamic dispatch if there are different types of nodes for example. But it would still be the Visitor pattern if there was only one type of node and no reason for dynamic dispatch.

Dynamic dispatch is a powerful feature in Clojure, but trying to justify it as a giant killer to the Visitor pattern shows a lack of understanding of Visitor pattern, and weakens the argument. Present the languages strengths, not your own weaknesses.

2 comments:

  1. Thanks for the article!

    Unfortunetely, alter-meta! doesn't make variable dynamic. Please consider the following:

    user=> (def x 42)
    #'user/x

    user=> (alter-meta! #'x assoc :dynamic true)
    {:ns #, :name x, :dynamic true, :line 1, :file "NO_SOURCE_PATH"}

    user=> (.isDynamic #'x)
    false

    user=> (binding [x 32] (println x))
    IllegalStateException Can't dynamically bind non-dynamic var: user/x clojure.lang.Var.pushThreadBindings (Var.java:353)

    But one can use .setDynamic instead:

    user=> (.setDynamic #'x)

    #'user/x
    user=> (binding [x 32] (println x))
    32
    nil

    ReplyDelete
  2. Curious, using alter-meta! worked for me, but then the whole language and environment is evolving. Who knows what other factors were behind my problem or contribute to the error in your example.

    However, setDynamic is far more obvious and straight forward. From a design point of view a much superior solution to the problem of setting a symbol to be suitable for dynamic binding.

    ReplyDelete