views:

121

answers:

1

I've been experimenting with monads in Clojure and came up with the following code, where a monadic value/state pair is represented by a mutable Clojure deftype object.

Since the object is mutable, an advantage would seem to be that you can write monadic code without needing to construct new result objects all the time.

However, I'm pretty new to monads so would love to know:

  • Does this construct make sense?
  • Will it actually work correctly as a monad?

Code below:

(defprotocol PStateStore 
  (set-store-state [ss v])
  (get-store-state [ss])
  (set-store-value [ss v])
  (get-store-value [ss]))

(deftype StateStore [^{:unsynchronized-mutable true} value 
                     ^{:unsynchronized-mutable true} state]
  PStateStore 
      (get-store-state [ss] (.state ss))
      (get-store-value [ss] (.value ss))
      (set-store-state [ss v] (set! state v))
      (set-store-value [ss v] (set! value v))

   Object
     (toString [ss] (str "value=" (.value ss) ", state=" (.state ss))))

(defn state-store [v s] (StateStore. v s))

(defmonad MStoredState
  [m-result (fn [v] 
              (fn [^StateStore ss] 
                  (do
                    (set-store-value ss v)
                    ss)))
   m-bind (fn [a f]
            (fn [^StateStore ss]
              (do
                (a ss)
                ((f (get-store-value ss)) ss))))])

; Usage examples

(def mb
  (domonad MStoredState
    [a (m-result 1)
     b (m-result 5)]
    (+ a b)))

(def ssa (state-store 100 101))

(mb ssa)

; => #<StateStore value=6, state=101>
+1  A: 

No, it won't work correctly as a monad, because you use mutable state.

Imagine that you have a monadic value m (a value, carrying a state), which you call a StateStore. You want to be able to do this :

(let
   [a (incr-state m)
    b (decr-state m)]
  (if some-condition a b))

What I expect is that this computation returns the monadic m whose state has been either, according to some-condition, incremented or decremented. If you use mutable state, it will be both incremented and decremented during evaluation of this code.

One of the good things about monads is that, while they represent effects, they behave as ordinary pure, non-mutable values. You can pass them around, duplicate them (you can expand any let-definition of a monadic value, replacing its name with it's definition at every use site). The only place you have to be careful is where you actually chain effect using m-bind. Otherwise, there is no implicit chaining of effects in unrelated parts of code, as in usual imperative programming. This is what makes reasoning about monads easier and more comfortable, in the situations where you want to restrict side-effects.

Edit

You may have heard of the monadic laws, which are equations that any monad implementation should respect. However, your problem here is not that you break the law, as the law don't talk about this. Indeed, the monadic laws are usually stated in the pure language Haskell, and therefore do not consider side-effects.

If you want, you could consider it as a fourth, unspoken, monad law : good monads should respect referential transparency

gasche