8

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.

ire_and_curses
  • 68,372
  • 23
  • 116
  • 141
Timothy Pratley
  • 10,586
  • 3
  • 34
  • 63

2 Answers2

16

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
  • 71,150
  • 28
  • 166
  • 168
  • I think encapsulation is not about "hiding things from yourself". It's about communicating your design to others first of all. – Alexey May 17 '12 at 18:39
0

I'm sure it is not idiomatic Clojure but you can definitely simulate protected variables.

In your example c is a simulated private variable. If you want it to be a protected variable, you need to define it in a way that allows both make-counter and make-bi-counter to access it. Pass a hash called secret into make-counter. If secret contains c, use that. Create your own otherwise.

Then make-bi-counter can create a secret object which contains c and pass it into the make-counter constructor. Now both make-bi-counter and make-counter have access to the same c.

oillio
  • 4,748
  • 5
  • 31
  • 37