Created
October 28, 2018 15:35
-
-
Save darwin/883b9fd2b9012d79d4bbb14a641d36ab to your computer and use it in GitHub Desktop.
My quick stab at state monads in Clojure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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