Multimethods are a great abstraction in Clojure.  They are great for
adapting a wide variety of inputs to a function.  However, I often
need to decouple the adaptation of data from the function that
operates on the adapted data.  Also, after the work is complete, I
need to adapt the data back to the original format.

As such, I've found the need to create visitor closures in my code.
Along the way I came up with a helper function to generate visitor
closures.  I'd like feedback on the visitor generator.

* Getting Motivated: String functions *

I write the following code a lot to deal with keywords & symbols:

(keyword (some-str-fn (name a-keyword) str-fn-args))
(symbol  (some-str-fn (name a-symbol ) str-fn-args))

Each of these can be abstracted out as
(defn visit-keyword
 [str-fn a-keyword & args]
    (keyword
      (apply str-fn (name a-keyword) args)))

(defn visit-symbol
 [str-fn a-symbol & args]
    (symbol
      (apply str-fn (name a-symbol ) args)))

So, the code is now called as follows

(visit-keyword str-fn a-keyword args)
(visit-symbol  str-fn a-symbol args)

This is one possible implementation of a visitor patter for symbols,
keywords & strings.

* More Problems: Map Functions *

It's common to filter on values or keys when working with maps.  I've
written a lot of code like this:

;keys
(into {} (some-pred-fn (comp a-pred key) map-fn-args))
;values
(into {} (some-pred-fn (comp a-pred val) map-fn-args))

A first approach would be to write specific helper function for each
case.  However, this can be turned into a visitor as well.

(defn visitor-keys-pred
  [pred-fn pred-arg & args]
    (into {}
      (apply pred-fn (#(comp % key) pred-arg)
        args)))

(defn visitor-vals-pred
  [pred-fn pred-arg & args]
    (into {}
      (apply pred-fn (#(comp % val) pred-arg)
        args)))

The definition looks a little funky, but we'll see why in a minute.
For now the functions can be called as follows:

(visitor-keys-pred pred-fn pred-arg args)
(visitor-vals-pred pred-fn pred-arg args)

* Putting it together:  Visitor Closures *

As you can see, a pattern is starting to develop.  Each visitor
function has a similar signature:

[f first-arg & rest-args]

This obviously becomes

[f & args]

Each visitor follows a similar pattern, too.
1.  Apply a visit-fn to get to a common base type
2.  Do work on the base type
3.  Apply a return-fn to get back to the original type.

We can turn this description into a closure generating function:

(defn visitor
  "Used to implement visitor patterns. (first (args)) is modified by
the visitor function, and the result is wrapped in a return-fn call."
  [visit-fn return-fn]
  (fn [f & args]
    (return-fn
     (apply f
       (visit-fn (first args))
       (rest args)))))

We now have a new way to define previous visitor functions.  Here's
what they look like using this new helper function:

(def visit-keyword (visitor name keyword))
(def visit-symbol  (visitor name symbol ))

;Works w/ predicate functions
(def visit-keys-pred (visitor #(comp % key) (partial into {}))
(def visit-vals-pred (visitor #(comp % val) (partial into {}))

As you can see, the only thing we described is the visit-fn and return-
fn.  The rest of the behavior is defined by the visitor pattern
itself.

* Second form *

The one catch is that visitor assumes that the dispatched data is the
first argument to f.  Sometimes it is the last argument to f, as in
take & drop.  It is easy enough to define visitor* that works on the
last argument of a function.  The definition is in the link.

http://gist.github.com/241144

* Back to Multimethods *

The power of the individual closures can be amplified when wrapped in
a multimethod.  Consider our String/Symbol/Keyword group.

(defmulti visit-string (fn [& args] (second args))

(defmethod visit-string clojure.lang.Symbol
 [f & args]
 (apply visit-symbol f args))

(defmethod visit-string clojure.lang.Keyword
 [f & args]
 (apply visit-keyword f args))

(defmethod visit-string :default
 [f & args]
 (apply f args))

We now have a visit-string function that can truly visit anything.
Our function calls above become:

(visit-string str-fn a-keyword args)
(visit-string str-fn a-symbol  args)

* Closing questions *

So now that you've how & why this works, I've got some questions for
the group:

* Did I accidentally duplicate functionality in core?
* How can this be more flexible?
* What maintenance problems are there that I don't see?
* Will speed be an issue?
* Is the signature of visitor sensible?  Is the signature of the
generated function sensible?  How could it be better?

Thanks in advance,
Sean

-- 
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