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))
(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
| 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:
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)))
(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