Skip to content

Commit cee8358

Browse files
authored
Re-add state monad (#210)
* re-add state monad
1 parent e9060d5 commit cee8358

File tree

6 files changed

+233
-3
lines changed

6 files changed

+233
-3
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
# Changelog #
22

3+
## Version 2.3.0 ##
4+
5+
Date: 2018-08-29
6+
7+
- Add state monad implementation
8+
39
## Version 2.2.0 ##
410

511
Date: 2018-01-11
612

713
- Fix some issues with wrong handling of dynamic context.
814
- Convert some functions to macros for delay args evaluation.
915

10-
1116
## Version 2.1.0 ##
1217

1318
Date: 2017-04-20

doc/content.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
= Cats Documentation
22
Andrey Antukh & Alejandro Gómez
3-
2.2.0
3+
2.3.0
44
:toc: left
55
:!numbered:
66
:idseparator: -

project.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
(defproject funcool/cats "2.2.0"
1+
(defproject funcool/cats "2.3.0"
22
:description "Category Theory abstractions for Clojure"
33
:url "https://github.com/funcool/cats"
44
:license {:name "BSD (2 Clause)"

src/cats/monad/state.cljc

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
(ns cats.monad.state
2+
(:refer-clojure :exclude [eval get])
3+
(:require [cats.context :as ctx :refer [*context*]]
4+
[cats.core :as m]
5+
[cats.data :as d]
6+
[cats.protocols :as p]
7+
[cats.util :as util]))
8+
9+
(declare context)
10+
11+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
12+
;; Protocol declaration
13+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
14+
15+
(defprotocol MonadState
16+
"A specific case of Monad abstraction for
17+
work with state in pure functional way."
18+
(-get-state [m] "Return the current state.")
19+
(-put-state [m newstate] "Update the state.")
20+
(-swap-state [m f] "Apply a function to the current state and update it."))
21+
22+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
23+
;; Type constructors and functions
24+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
25+
26+
(defrecord State [mfn state-context]
27+
p/Contextual
28+
(-get-context [_] state-context)
29+
30+
p/Extract
31+
(-extract [_] mfn))
32+
33+
(alter-meta! #'->State assoc :private true)
34+
35+
(defn state
36+
"The State type constructor.
37+
The purpose of State type is wrap a simple
38+
function that fullfill the state signature.
39+
It exists just for avoid extend the clojure
40+
function type because is very generic type."
41+
([f]
42+
(State. f context))
43+
([f state-context]
44+
(State. f state-context)))
45+
46+
(defn state?
47+
"Return true if `s` is instance of
48+
the State type."
49+
[s]
50+
(instance? State s))
51+
52+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
53+
;; Monad definition
54+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
55+
56+
(def ^{:no-doc true}
57+
context
58+
(reify
59+
p/Context
60+
61+
p/Extract
62+
(-extract [mv] (p/-extract mv))
63+
64+
p/Functor
65+
(-fmap [_ f fv]
66+
(state (fn [s]
67+
(let [[v ns] ((p/-extract fv) s)]
68+
(d/pair (f v) ns)))))
69+
70+
p/Monad
71+
(-mreturn [_ v]
72+
(state (partial d/pair v)))
73+
74+
(-mbind [_ self f]
75+
(state (fn [s]
76+
(let [p ((p/-extract self) s)
77+
value (.-fst p)
78+
newstate (.-snd p)]
79+
((p/-extract (f value)) newstate)))))
80+
81+
MonadState
82+
(-get-state [_]
83+
(state #(d/pair %1 %1)))
84+
85+
(-put-state [_ newstate]
86+
(state #(d/pair % newstate)))
87+
88+
(-swap-state [_ f]
89+
(state #(d/pair %1 (f %1))))
90+
91+
p/Printable
92+
(-repr [_]
93+
#"<State>")))
94+
95+
(util/make-printable (type context))
96+
97+
(defn ^:private get-context
98+
"Default to context if no context set"
99+
[]
100+
(if (nil? *context*)
101+
context
102+
*context*))
103+
104+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
105+
;; Public Api
106+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
107+
108+
(defn get
109+
"Return a State instance with computation that returns
110+
the current state."
111+
[]
112+
(ctx/with-context (get-context)
113+
(-get-state (ctx/infer))))
114+
115+
(defn put
116+
"Return a State instance with computation that replaces
117+
the current state with specified new state."
118+
[newstate]
119+
(ctx/with-context (get-context)
120+
(-put-state (ctx/infer) newstate)))
121+
122+
(defn swap
123+
"Return a State instance with computation that applies the
124+
specified function to state and returns the old state."
125+
[f]
126+
(ctx/with-context (get-context)
127+
(-swap-state (ctx/infer) f)))
128+
129+
(defn run
130+
"Given a State instance, execute the
131+
wrapped computation and returns a cats.data.Pair
132+
instance with result and new state.
133+
(def computation (mlet [x (get-state)
134+
y (put-state (inc x))]
135+
(return y)))
136+
(def initial-state 1)
137+
(run-state computation initial-state)
138+
This should return something to: #<Pair [1 2]>"
139+
[state seed]
140+
((p/-extract state) seed))
141+
142+
(defn eval
143+
"Given a State instance, execute the
144+
wrapped computation and return the resultant
145+
value, ignoring the state.
146+
Equivalent to taking the first value of the pair instance
147+
returned by `run-state` function."
148+
[state seed]
149+
(first (run state seed)))
150+
151+
(defn exec
152+
"Given a State instance, execute the
153+
wrapped computation and return the resultant
154+
state.
155+
Equivalent to taking the second value of the pair instance
156+
returned by `run-state` function."
157+
[state seed]
158+
(second (run state seed)))
159+
160+
(defn gets
161+
"State monad that returns the result of applying
162+
a function to a state"
163+
[projfn]
164+
(m/mlet [s (get)]
165+
(m/return (projfn s))))
166+
167+
(defn wrap-fn
168+
"Wraps a (possibly side-effecting) function to a state monad"
169+
[my-fn]
170+
(state (fn [s]
171+
(d/pair (my-fn) s))))

test/cats/monad/state_spec.cljc

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
(ns cats.monad.state-spec
2+
#?@(:clj
3+
[(:require
4+
[cats.context :as ctx]
5+
[cats.core :as m]
6+
[cats.data :as d]
7+
[cats.monad.state :as state]
8+
[clojure.test :as t])]
9+
:cljs
10+
[(:require
11+
[cats.context :as ctx :include-macros true]
12+
[cats.core :as m :include-macros true]
13+
[cats.data :as d]
14+
[cats.monad.state :as state]
15+
[cljs.test :as t])]))
16+
17+
(def postincrement
18+
(m/mlet [x (state/get)
19+
_ (state/put (+ x 1))]
20+
(m/return x)))
21+
22+
(t/deftest state-monad-tests
23+
24+
(t/testing "state"
25+
(let [mstate (state/state (fn [st] (d/pair "foo" (* 2 st))))]
26+
(t/is (= (state/state? mstate) true))
27+
(t/is (= (state/run mstate 2) (d/pair "foo" 4)))))
28+
29+
(t/testing "monad operations"
30+
(t/is (= (state/run (ctx/with-context state/context (m/return 1)) 0) (d/pair 1 0)))
31+
(let [mstate1 (state/get)
32+
func (fn [value] (state/state (fn [st] [(+ 2 st) (+ value st)])))
33+
mstate2 (m/bind mstate1 func)]
34+
(t/is (= (state/state? mstate2) true))
35+
(t/is (state/run mstate2 1) [3 2])))
36+
37+
(t/testing "put"
38+
(let [put-hello (state/put "hello")]
39+
(t/is (= (state/run put-hello "x") (d/pair "x" "hello")))))
40+
41+
(t/testing "get"
42+
(t/is (= (state/run (state/get) "x") (d/pair "x" "x"))))
43+
44+
(t/testing "swap"
45+
(let [appendworld (state/swap (fn [st] (str st " world!")))]
46+
(t/is (= (state/exec appendworld "hello") "hello world!"))))
47+
48+
(t/testing "wrap-fn"
49+
(t/is (= (state/run (state/wrap-fn (fn [] (+ 2 3))) 0) (d/pair 5 0))))
50+
51+
(t/testing "post-increment"
52+
(t/is (= (state/run postincrement 1) (d/pair 1 2)))))

test/cats/runner.cljs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
[cats.monad.exception-spec]
77
[cats.monad.either-spec]
88
[cats.monad.maybe-spec]
9+
[cats.monad.state-spec]
910
[cats.monad.identity-spec]
1011
[cats.labs.sugar-spec]
1112
[cats.labs.channel-spec]
@@ -22,6 +23,7 @@
2223
'cats.monad.either-spec
2324
'cats.monad.maybe-spec
2425
'cats.monad.identity-spec
26+
'cats.monad.state-spec
2527
'cats.labs.sugar-spec
2628
'cats.labs.channel-spec
2729
'cats.labs.promise-spec))

0 commit comments

Comments
 (0)