HiveBrain v1.2.0
Get Started
← Back to all entries
patternMinor

Encapsulated state in clojure

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
clojureencapsulatedstate

Problem

While going through SICP and trying to implement the code in clojure, I've found that while I can get the code in chapter 3 to work, it seems to go against Clojure idioms, but I can't quite imagine the "Clojure way" to do it either.

Take for example, the digital circuit simulator. This is my implementation:

(declare call-each)
(defn make-wire []
  (let [signal-value (atom 0)
        action-procedures (atom '())
        set-my-signal (fn [new-value]
                        (if-not (= @signal-value new-value)
                          (do (reset! signal-value new-value)
                              (call-each @action-procedures))
                          'done))
        accept-action-procedure! (fn [proc]
                                   (swap! action-procedures
                                          conj proc)
                                   (proc))
        dispatch (fn [m]
                   (cond (= m 'get-signal) @signal-value
                         (= m 'set-signal) set-my-signal
                         (= m 'add-action) accept-action-procedure!
                         :else (error "Unknown operation -- WIRE" m)))]
    dispatch))

(defn call-each [[proc & rest :as procedures]]
  (if (empty? procedures)
    'done
    (do (proc)
        (call-each rest))))

(defn get-signal [wire]
  (wire 'get-signal))
(defn set-signal! [wire new-value]
  ((wire 'set-signal) new-value))
(defn add-action! [wire action-procedure]
  ((wire 'add-action) action-procedure))


As you can see, it has an internal signal-value and action-procedures as state. Is this the natural way to do this in Clojure, and if not, how would you do it?

Solution

There are a few problems.

-
You should not read the value from an atom and update it conditional on its value, as in set-my-signal. It's value may have changed in between those two calls. In this case refs instead of atoms may need to be used.

-
Do not use unbounded recursion as in call-each.

Instead of:

(defn call-each [[proc & rest :as procedures]]
  (if (empty? procedures)
    'done
    (do (proc)
        (call-each rest))))


You can do:

(defn- call-each [procedures]
  (doseq [proc procedures] (proc)))


-
error is not a clojure function. So you should have implemented in clojure with throw... Just use throw instead. It would read something like: (throw (UnsupportedOperationException. (str "Unknown operation -- WIRE " m) nil)). Or instead of throwing an exception you can return nil. So it would be easier to extend (add new messages by decorating) the make-wire constructor.

-
Case analysis (cond) on a constant symbols is someone does a lot in Scheme. Clojure maps are more readable though. Also using keywords :get-signal instead of symbols 'get-signal is more conventional in accessing the fields of a map, record, so on..

make-wire would be read something like this, if I wrote it in Clojure:

(defn make-wire []
  (let [signal-value (ref 0)
        action-procedures (ref '())]
    {
     :get-signal (fn [] @signal-value)
     :set-signal (fn [new-value]
                   (dosync
                     (when-not (= @signal-value new-value) 
                       (ref-set signal-value new-value)
                       (call-each @action-procedures))))
     :add-action (fn [proc]
                   (dosync
                     (alter action-procedures conj proc)
                     (proc)))
     }))


The point of encapsulation as taught in SICP is that you can combine components independent of their implementation details.

So you might want to define a protocol explicitly for a wire:

(defprotocol Wire
  (get-signal [w])
  (set-signal! [w new-value])
  (add-action! [w action-procedure]))


Then whether you implement it using a record:

(defrecord WireRec [signal-value action-procedures]
  Wire
  (get-signal [_] @signal-value)
  (set-signal! [_ new-value]
    (dosync
      (when-not (= @signal-value new-value)
        (ref-set signal-value new-value)
        (call-each @action-procedures))))
   (add-action! [_ proc]
     (dosync
       (alter action-procedures conj proc)
       (proc))))

(defn make-wire2 []
  (WireRec. (ref 0) (ref '())))


Or a map:

(defn make-wire3 []
  (let [m (ref {
                :signal-value 0
                :action-procedures '()
                })]
  (reify Wire
    (get-signal [_] (:signal-value @m))
    (set-signal! [_ new-value]
          (dosync
            (when-not (= (:signal-value @m) new-value)
              (alter m assoc :signal-value new-value)
              (call-each (:action-procedures @m)))))
    (add-action! [_ proc]
      (dosync
        (alter m update-in [:action-procedures] conj proc)
        (proc))))))


should not matter.

Code Snippets

(defn call-each [[proc & rest :as procedures]]
  (if (empty? procedures)
    'done
    (do (proc)
        (call-each rest))))
(defn- call-each [procedures]
  (doseq [proc procedures] (proc)))
(defn make-wire []
  (let [signal-value (ref 0)
        action-procedures (ref '())]
    {
     :get-signal (fn [] @signal-value)
     :set-signal (fn [new-value]
                   (dosync
                     (when-not (= @signal-value new-value) 
                       (ref-set signal-value new-value)
                       (call-each @action-procedures))))
     :add-action (fn [proc]
                   (dosync
                     (alter action-procedures conj proc)
                     (proc)))
     }))
(defprotocol Wire
  (get-signal [w])
  (set-signal! [w new-value])
  (add-action! [w action-procedure]))
(defrecord WireRec [signal-value action-procedures]
  Wire
  (get-signal [_] @signal-value)
  (set-signal! [_ new-value]
    (dosync
      (when-not (= @signal-value new-value)
        (ref-set signal-value new-value)
        (call-each @action-procedures))))
   (add-action! [_ proc]
     (dosync
       (alter action-procedures conj proc)
       (proc))))

(defn make-wire2 []
  (WireRec. (ref 0) (ref '())))

Context

StackExchange Code Review Q#72171, answer score: 4

Revisions (0)

No revisions yet.