views:

319

answers:

1

Here is an overly simplistic example for illustration:

I can encapsulate an implementation detail such as using an atom for a counter:

(defn make-counter
  ([] (make-counter 0))
  ([init-val]
   (let [c (atom init-val)]
     {:get (fn [] @c)
      :++ (fn [] (swap! c inc))})))

But that means I need to redefine everything to add a feature (no inheritance):

(defn make-bi-counter
  ([] (make-bi-counter 0))
  ([init-val]
   (let [c (atom init-val)]
     {:get (fn [] @c)
      :++ (fn [] (swap! c inc))
      :-- (fn [] (swap! c dec))})))

Whereas if it were possible to just extend the one function:

(assoc c :-- (env (:++ c) (fn [] (swap! c dec))))

(def c (make-counter))
(def b (make-bi-counter))
user=> ((:-- b))
-1
user=> ((:-- b))
-2
user=> ((:get b))
-2

Or I could have just exposed the atom and had independent functions:

(defn -- [a] (swap! a dec))
(def a (atom 0))
(-- a)

It appears the best option is to forgo encapsulation, if 'inheritance' (or perhaps more accurately: extension) is desirable.

+9  A: 

Yes, I think idiomatic Clojure is to separate your data from your functions, for precisely the reason that you can later write new functions to work on the old data.

Bundling functions with data also means you can't alter your functions later without altering or re-generating all of your data structures, since you'll have these anonymous functions stored all over the place. Developing interactively at a REPL, I'd hate to have to hunt down all of my data structures to fix them every time I change a function. Closures in hash-maps are clever but they're pretty fragile and I wouldn't go that route unless there was a really good reason.

It only takes a modicum of discipline to define your interface (as functions) and then remember to stick to your interface and not to mess around with the atom directly. It's unclear what benefit you're going to get from forcibly hiding things from yourself.

If you want inheritance, multimethods are a good way to do it.

(defmulti getc type)
(defmulti ++ type)
(defmulti -- type)

(derive ::bi-counter ::counter)

(defn make-counter
  ([] (make-counter 0))
  ([init-val]
     (atom init-val :meta {:type ::counter})))

(defn make-bi-counter
  ([] (make-bi-counter 0))
  ([init-val]
     (atom init-val :meta {:type ::bi-counter})))

(defmethod getc ::counter [counter]
  @counter)

(defmethod ++ ::counter [counter]
  (swap! counter inc))

(defmethod -- ::bi-counter[counter]
  (swap! counter dec))

e.g.

user> (def c (make-counter))
#'user/c
user> (getc c)
0
user> (def b (make-bi-counter))
#'user/b
user> (++ c)
1
user> (++ b)
1
user> (-- b)
0
user> (-- c)
; Evaluation aborted.
;;  No method in multimethod '--' for dispatch value: :user/counter
Brian Carper