Skip to content

Instantly share code, notes, and snippets.

@darwin
Created October 28, 2018 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save darwin/883b9fd2b9012d79d4bbb14a641d36ab to your computer and use it in GitHub Desktop.
Save darwin/883b9fd2b9012d79d4bbb14a641d36ab to your computer and use it in GitHub Desktop.
My quick stab at state monads in Clojure
(ns experimental.state-monad
(:refer-clojure :exclude [let]))
; This is a poor man's attempt to get something like state monad.
; We could have used clojure.algo.monads, but I wanted to avoid these drawbacks:
; https://www.reddit.com/r/Clojure/comments/8pyxk8/motivation_for_monads/e0gb6on/
; Alternatively we could have used some heavy category theory library like cats or fluokitten, but
; that looked like a pretty complex thing to learn and keep in head.
; Also I want this thing to play well with Cursive.
;
; I had an idea to introduce a lightweight monad-aware-let macro with some conventions this way:
;
; Monadic functions accept state as the first parameter and return a pair [result new-state].
;
; Normally I would thread state through some monadic calls using let destructuring and shadowing:
; (let [state {:some "value"}
; [res1 state] (monadic-fn1 state 1 2 3)
; [_ state] (monadic-fn2 state 4 res1)
; _ (side-effecting-non-monadic-fn 1 2 res2 state)
; [res3 state] (monadic-fn3 state)]
; state)
;
; This new let macro is aware of monadic functions and allows above snippet to be written as:
; (sm/let [state {:some "value"}
; res1 (monadic-fn1 1 2 3)
; _ (monadic-fn2 4 res1)
; _ (side-effecting-non-monadic-fn 1 2 res2 state)
; res3 (monadic-fn3)])
;
; It simply detects all rows with monadic calls and desugars them.
;
; Please note that monadic functions must be marked as ^::monadic
;
; -- monadic value structure ------------------------------------------------------------------------------------------------
(defn value [result state]
[result state])
(defn get-result [value]
(first value))
(defn get-state [value]
(second value))
(defn gen-destructuring [state-sym result-destructuring]
(assert (symbol? state-sym))
`[~result-destructuring ~state-sym])
; -- macro impl -------------------------------------------------------------------------------------------------------------
(defn monadic? [sym]
(true? (::monadic (meta (resolve sym)))))
(defn gen-monad-aware-row [state-sym row]
(clojure.core/let [[destructuring code] row]
(if (and (list? code) (monadic? (first code)))
`[~(gen-destructuring state-sym destructuring) (~(first code) ~state-sym ~@(rest code))]
row)))
(defn gen-let [bindings body]
(clojure.core/let [rows (partition 2 bindings)
[state-sym state-val] (first rows)
monad-aware-rows (mapcat (partial gen-monad-aware-row state-sym) (rest rows))
body (if (empty? body)
(list state-sym)
body)]
`(clojure.core/let [~state-sym ~state-val
~@monad-aware-rows]
(do ~@body))))
(defmacro let [bindings & body]
(gen-let bindings body))
; -- playground -------------------------------------------------------------------------------------------------------------
(comment
(do
(defn ^::monadic m-set [state v]
(value v (assoc-in state [:k] v)))
(defn ^::monadic m-inc [state & extra-args]
(value extra-args (update-in state [:k] inc)))
(defn ^::monadic m-add [state n]
(let [res (+ (get-in state [:k]) n)]
(value res (assoc-in state [:k] res)))))
;
(macroexpand-1 '(let [s {:k 1}
_ (m-set 42)
_ (println (get s :k))
res (m-inc 2 3)]
res))
;
(macroexpand-1 '(let [s {:k 1}
_ (m-set 42)
s {:k 30}
_ (println (get s :k))
res (m-add 70)])))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment