diff --git a/src/cats/monad.cljc b/src/cats/monad.cljc new file mode 100644 index 0000000..b0d24cc --- /dev/null +++ b/src/cats/monad.cljc @@ -0,0 +1,198 @@ +(ns cats.monad + (:require [cats.protocols :as p])) + +;; + +(defprotocol Computation + (-run [computation context]) + (-step? [computation])) + +(defn mrun + [computation context] + (let [nxt (-run computation context)] + (if (-step? nxt) + (recur nxt context) + nxt))) + +;; + +(deftype Apply [f vs] + Computation + (-run [_ ctx] + (transduce (map #(-run % ctx)) + (completing + (fn [f v] + (p/-fapply ctx f v))) + (-run f ctx) + vs)) + + (-step? [_] true)) + +(defn fapply + [f & vs] + (Apply. f vs)) + +(deftype Return [x] + Computation + (-run [_ ctx] + (p/-mreturn ctx x)) + + (-step? [_] true)) + +(defn return + [x] + (Return. x)) + +(deftype Bind [m k] + Computation + (-run [_ ctx] + (p/-mbind ctx + (mrun m ctx) + (comp #(mrun % ctx) k))) + + (-step? [_] true)) + +(defn bind + [m k] + (Bind. m k)) + +(deftype Zero [] + Computation + (-run [_ ctx] + (p/-mzero ctx)) + + (-step? [_] true)) + +(defn mzero + [] + (Zero.)) + +(deftype Plus [a b] + Computation + (-run [_ ctx] + (p/-mplus ctx a b)) + + (-step? [_] true)) + +(defn mplus + [a b] + (Plus. a b)) + +;; + +(extend-protocol Computation + #?(:clj Object :cljs object) + (-run [o _] o) + (-step? [_] false) + + nil + (-run [_ _] nil) + (-step? [_] false)) + +;; macros + +(defmacro mdo + [bindings & body] + (when-not (and (vector? bindings) + (not-empty bindings) + (even? (count bindings))) + (throw (IllegalArgumentException. "bindings has to be a vector with even number of elements."))) + (->> (reverse (partition 2 bindings)) + (reduce (fn [acc [l r]] + (case l + :let `(let ~r ~acc) + :when `(Bind. (if ~r + (Return. nil) + (Zero.)) + (fn [~(gensym)] ~acc)) + `(Bind. ~r (fn [~l] ~acc)))) + `(do ~@body)))) + +(comment + (require '[cats.builtin :as b]) + (require '[cats.monad.maybe :as maybe]) + + (mrun (return 42) b/sequence-context) + (mrun (return 42) maybe/context) + + (mrun (bind (return 42) + (fn [x] (return (inc x)))) + maybe/context) + + (mrun (bind (return 42) (fn [x] (return (inc x)))) + b/sequence-context) + + (mrun (mdo [x (return 2) + y (return 21)] + (return (* x y))) + maybe/context) + + (mrun (mdo [x (return 2) + y (return 21)] + (return (* x y))) + b/sequence-context) + + (mrun (mdo [x [1 2 3] + y [4 5 6] + :let [z (+ x y)] + :when (even? z)] + (return z)) + b/sequence-context) + + (mrun + (fapply (return inc) (return 2)) + maybe/context) + + (mrun + (fapply (fapply (return (fn [x] + (fn [y] + (* x y)))) + (return 2)) + (return 21)) + maybe/context) + + (mrun + (fapply (return (fn [x] + (fn [y] + (* x y)))) + (return 2) + (return 21)) + maybe/context) + + ;; + + (require '[cats.core :as core]) + (require '[cats.context :as ctx]) + + (defmacro expand + [n] + (let [bindings (interleave (repeatedly n gensym) + (repeat `(core/return 1)))] + (println :bindings bindings) + `(core/mlet [~@bindings] + (core/return 42)))) + + ;; 1000 + (ctx/with-context maybe/context + (expand 1e3)) + + (clojure.pprint/pprint + (clojure.walk/macroexpand-all '(expand 10))) + + + ;; FIXME + (mrun + (last + (take 1e3 + (iterate (fn [computation] + (Bind. computation (fn [x] + (Return. (inc x))))) + (Return. 0)))) + + maybe/context) + ) + + + + + diff --git a/test/cats/monad_spec.cljc b/test/cats/monad_spec.cljc new file mode 100644 index 0000000..f0fdb1c --- /dev/null +++ b/test/cats/monad_spec.cljc @@ -0,0 +1,61 @@ +(ns cats.monad-spec + #?(:cljs + (:require [cljs.test :as t] + [cats.builtin :as b] + [cats.monad.maybe :as maybe] + [cats.monad :as m :include-macros true] ) + :clj + (:require [clojure.test :as t] + [cats.builtin :as b] + [cats.monad.maybe :as maybe] + [cats.monad :as m] + [cats.context :as ctx]))) + +(t/deftest simple-monadic-computations + (let [program (m/mdo [x (m/return 2) + y (m/return 21)] + (m/return (* x y)))] + (t/testing "Can be used with a sequence monad" + (t/is (= [42] + (m/mrun program b/sequence-context)))) + (t/testing "Can be used with a maybe monad" + (t/is (= (maybe/just 42) + (m/mrun program maybe/context)))))) + +(t/deftest nested-monadic-computations + (let [program (m/mdo [x (m/return 1) + y (m/return 2) + z (m/mdo [a (m/return 1) + b (m/return 2) + c (m/mdo [p (m/return 2) + q (m/return 2)] + (m/return (* p q)))] + (m/return (+ a b c)))] + (m/return (+ x y z)))] + (t/testing "Can be used with a sequence monad" + (t/is (= [10] + (m/mrun program b/sequence-context)))) + (t/testing "Can be used with a maybe monad" + (t/is (= (maybe/just 10) + (m/mrun program maybe/context)))))) + +(t/deftest inline-lets + (let [program (m/mdo [x (m/return 1) + y (m/return 2) + :let [z (+ x y)] + a (m/return z) + b (m/return 4)] + (m/return (+ a b)))] + (t/testing "Can be used to store intermediate results" + (t/is (= [7] + (m/mrun program b/sequence-context)))))) + +(t/deftest guards + (t/testing "Can be used to filter out values" + (let [program (m/mdo [x [1 2 3] + y [4 5 6] + :let [z (+ x y)] + :when (even? z)] + (m/return z))] + (t/is (= [6 6 8 8] + (m/mrun program b/sequence-context)))))) diff --git a/test/cats/runner.cljs b/test/cats/runner.cljs index 1ce72c3..c711a06 100644 --- a/test/cats/runner.cljs +++ b/test/cats/runner.cljs @@ -2,6 +2,7 @@ (:require [clojure.string :as str] [cljs.test :as test] [cats.core-spec] + [cats.monad-spec] [cats.builtin-spec] [cats.applicative.validation] [cats.monad.exception-spec] @@ -26,6 +27,7 @@ (test/run-tests (test/empty-env) 'cats.core-spec + 'cats.monad-spec 'cats.builtin-spec 'cats.applicative.validation 'cats.monad.exception-spec