Thursday, April 26, 2012

Programming Clojure - Living Without Multimethods


 The best way to appreciate multimethods is to spend a few minutes living without them, so let’s do that. Clojure can already print anything with print/println. But pretend for a moment that these functions do not exist and that you need to build a generic print mechanism. To get started, create a my-print function that can print a string to the standard output stream <<out>>:
src/examples/life_without_multi.clj
(defn my-print [ob]
(.write *out* ob))
Next, create a my-println that simply calls my-print and then adds a line feed:

src/examples/life_without_multi.clj
(defn my-println [ob]
(my-print ob)
(.write *out* "\n"))
The line feed makes my-println’s output easier to read when testing at the REPL. For the remainder of this section, you will make changes to my-print and test them by calling my-println. Test that my-println works with strings: 
(my-println "hello")
| hello
-> nil
That is nice, but my-println does not work quite so well with nonstrings such as nil:

(my-println nil)
-> java.lang.NullPointerException
That’s not a big deal, though. Just use cond to add special-case handling for nil:

src/examples/life_without_multi.clj
(defn my-print [ob]
(cond
(nil? ob) (.write *out* "nil")
(string? ob) (.write *out* ob)))
With the conditional in place, you can print nil with no trouble:
(my-println nil)
| nil
-> nil
Of course, there are still all kinds of types that my-println cannot deal with. If you try to print a vector, neither of the cond clauses will match, and the program will print nothing at all:

(my-println [1 2 3])
-> nil
By now you know the drill. Just add another cond clause for the vector case.
The implementation here is a little more complex, so you might want to separate the actual printing into a helper function, such as my-print-vector:

src/examples/life_without_multi.clj
(require '[clojure.string :as str])
(defn my-print-vector [ob]
(.write *out*"[")
(.write *out* (str/join " " ob))
(.write *out* "]"))
(defn my-print [ob]
(cond
(vector? ob) (my-print-vector ob)
(nil? ob) (.write *out* "nil")
(string? ob) (.write *out* ob)))
Make sure that you can now print a vector:

(my-println [1 2 3])
| [1 2 3]
-> nil
my-println now supports three types: strings, vectors, and nil. And you have a road map for new types: just add new clauses to the cond in my-println. But it is a crummy road map, because it conflates two things: the decision process for selecting an implementation and the specific implementation detail.
You can improve the situation somewhat by pulling out helper functions like my-print-vector. However, then you have to make two separate changes every time you want to a add new feature to my-println:
·        Create a new type-specific helper function.
·        Modify the existing my-println to add a new cond invoking the feature-specific helper.
 
What you really want is a way to add new features to the system by adding new code in a single place, without having to modify any existing code. Clojure offers this by way of protocols,

No comments:

Post a Comment