-
Notifications
You must be signed in to change notification settings - Fork 15
/
walkthrough.repl
409 lines (345 loc) · 13.6 KB
/
walkthrough.repl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
(require '[state-flow.api :as flow :refer [run run* flow match?]])
;; An integration test can be expressed as a series of steps with
;; assertions mixed in. Ideally, all of the steps are pure functions,
;; meaning they don't access any external state. To support that, we
;; need to thread the state through the functions. We _could_ use Clojure's
;; -> macro to do this:
(-> {:count 0} ;; << state - we'll refer to it as s in the next steps
((fn [s] (update s :count inc)))
((fn [s] (assert (= 1 (:count s))) s)))
;; Each of those inline functions could be def'd ...
(defn inc-count [s] (update s :count inc))
(defn match [expected f] (fn [s] (assert (= expected (f s)))))
;; ... so we end up with a flow this:
(-> {:count 0}
inc-count
((match 1 :count)))
;; That's a bit more expressive, but there's a lot more we'd like to
;; have in a testing framework.
;;
;; Enter state-flow: a framework for defining integration test flows
;; using (mostly) pure functions of state, which is managed for you by
;; a runner that threads the state through the functions and provides
;; many additional services along the way.
;;
;; Here's an example. Don't worry about the details; we'll cover everything
;; below.
(run ;; << `run` takes a flow and an initial state
(flow "increment count" ;; << a `flow` is a collection of steps
(flow/swap-state update :count inc) ;; << step that updates state, incrementing :count
(match? 1 (flow/get-state :count)) ;; << step that asserts the :count is 1
(flow/get-state)) ;; << step that returns the state
{:count 0}) ;; << initial state
;; => [{:count 1} {:count 1}] ;; << result pair - we'll explain this later
;;
;; Note that we said "step that updates state". But we just said
;; that these functions should be pure! Well, actually the `flow/swap-state`
;; function itself just returns the result of applying `update`, in
;; this example, to whatever is passed to it, e.g.
(fn [s] (update s :count inc))
;; ... just like in the -> example above. As you'll see, it's the runner that
;; takes care of binding state to the function argument, and binding the
;; response back to the state context.
;;
;; ------------------------------------------------
;; introduction the state monad and primitive steps
;; ------------------------------------------------
;;
;; state-flow is implemented using a state monad. If you're already
;; familiar with monads, great! If not, don't worry. We'll explain
;; just enough about the state monad to understand state-flow.
;;
;; - A monad is a wrapper around a function.
;; - The wrapped function will be invoked by a monad runner later.
;; - The monad runner manages binding arguments to the function.
;; - A state monad is a monad whose function is that of some mutable state.
;; - The return value of a state monad is a pair of [<left-value> <right-value>].
;; - The <right-value> is always the state _after_ the function is
;; invoked. Note: not all functions modify the state.
;; - The <left-value> depends on the function, as we'll see below.
;;
;; The state monad is the underlying structure for any primitive step
;; or flow (collection of steps) in state-flow. state-flow includes a
;; number of constructors for primitive steps (state monads).
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; flow/get-state
;;
;; flow/get-state is a primitive step that returns the application of a
;; function to the state, leaving the state intact.
;; First, let's just see what it evaluates to by itself.
(flow/get-state :count)
;; => {:mfn #function[...],
;; :state-context #<State-E>}
(class *1)
;; => cats.monad.state.State
;;
;; - cats.monad.state.State is the class of the underlying object
;; - :mfn is the wrapped (monadic) function
;; - :state-context is a reference to mutable state, which the runner
;; will bind to the function's argument.
;;
;; The :mfn of a state monad is a function of the state. When you invoke
;; :mfn with some state, it returns a pair of [<left-value> <right-value>],
;; where the <left-value> is what the function actually returns and
;; <right-value> is the state after the function is applied. So you can
;; think of [<left-value> <right-value>] to mean [<return-value> <state>].
;;
;; Since this is just a Clojure function, you can extract it from the
;; step (monad) and invoke it directly:
(let [f (:mfn (flow/get-state :count))]
(f {:count 0}))
;; => [0 {:count 0}]
;;
;; state-flow provides a `run` function that binds the state to the
;; function argument for you:
(run (flow/get-state :count) {:count 0})
;; => [0 {:count 0}]
;;
;; The `run` function takes a step and an (optional - defaults to an
;; empty {}) initial state, and returns the return value from invoking
;; the :mfn with the state. Here's the same example with the default
;; initial-state:
(run (flow/get-state identity))
;; => [{} {}]
(run (flow/get-state (fn [s] (update s :count inc))) {:count 0})
;; return state
;; => [{:count 1} {:count 0}]
;;
;; The `{count 1}` on the left, the <left-value> or <return-value>, is
;; the result of applying the function we passed to `flow/get-state` to
;; the state. Since we passed `{count 0}` to `f`, that is passed to
;; `(fn [s] (update s :count + 1))`, which returns `{count 1}`.
;;
;; The `{count 0}` on the right, the <right-value> or <state>, is the
;; state _after_ invoking `flow/get-state`. Since `flow/get-state` does not
;; modify the state, it's the same value we handed to `f`.
;;
;; `flow/get-state` also supports compositional function chaining by
;; passing additional args to the function, like e.g. Clojure's
;; `update` function
(run (flow/get-state update :count inc) {:count 0})
;; => [{:count 1} {:count 0}]
(run (flow/get-state update :count + 2) {:count 0})
;; => [{:count 2} {:count 0}]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; flow/swap-state
;;
;; `flow/swap-state` is the complement of `flow/get-state`: it returns
;; the unmodified state and applies a function to the state:
(run (flow/swap-state (fn [s] (update s :count inc))) {:count 0})
;; => [{:count 0} {:count 1}]
;;
;; The `{count 0}` on the left, the <left-value> or <return-value>, is
;; the value of the state before `flow/swap-state`.
;;
;; The `{count 1}` on the right, the <right-value> or <state>, is the
;; the result of applying the function we passed to `flow/swap-state` to
;; the state. Since we passed `{count 0}` to `f`, that is passed to
;; `(fn [s] (update s :count + 1))`, which leaves the state `{count 1}`.
;;
;; And `flow/swap-state` also passes additional args to the function;
(run (flow/swap-state update :count inc) {:count 0})
;; => [{:count 0} {:count 1}]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; flow/return
;;
;; `flow/return` returns the value given to it, leaving state unchanged
(run (flow/return {:a 37}) {:b 42})
;; => [{:a 37} {:b 42}]
;; `flow/return` is most useful as the last step of a flow, to clearly
;; indicate what the flow will return when used within another flow.
;; ------------------------------------------------
;; flows
;; ------------------------------------------------
;;
;; We use flows to compose steps
(flow "c -> f"
(flow/swap-state (fn [s] (assoc s :f (:c s))))
(flow/swap-state update :f * 9)
(flow/swap-state update :f / 5)
(flow/swap-state update :f + 32)
(flow/get-state :f))
;; => {:mfn #object[...],
;; :state-context #<State-E>}
;;
;; And, then we can hand that directly to the run function, which
;; returns the result of the last step
(run (flow "c -> f"
(flow/swap-state (fn [s] (assoc s :f (:c s))))
(flow/swap-state update :f * 9)
(flow/swap-state update :f / 5)
(flow/swap-state update :f + 32)
(flow/get-state :f))
{:c 0})
;; => [32 {:c 0, :f 32}]
;; We can compose groups of steps
(defn c->f [k]
(flow "c -> f"
(flow/swap-state update k * 9)
(flow/swap-state update k / 5)
(flow/swap-state update k + 32)
(flow/get-state k)))
(defn copy-key [source target]
(flow/swap-state (fn [s] (assoc s target (get s source)))))
;; ... and then compose the compositions;
(run (flow "0c to f"
(copy-key :c :f)
(c->f :f)
(flow/get-state :f))
{:c 0})
;; => [32 {:c 0, :f 32}]
;; ------------------------------------------------
;; bindings
;; ------------------------------------------------
;;
;; bindings let you bind the returns of steps to symbols,
;; which are then in scope for the remainder of the flow.
(run
(flow "binding example"
[c (flow/get-state :c)]
(copy-key :c :f)
(c->f :f)
[f (flow/get-state :f)]
(flow/return [c f]))
{:c 0})
;; => [[0 32] {:c 0 :f 32}]
;; These look a lot like `let` bindings, but the symbol on the left
;; will be bound to the return of the monad on the right. You can also
;; bind _values_ using the `:let` keyword:
(run
(flow "binding example"
[:let [c 0]]
(flow/swap-state assoc :c 0)
(copy-key :c :f)
(c->f :f)
[f (flow/get-state :f)]
(flow/return [c f])))
;; => [[0 32] {:c 0 :f 32}]
;; ------------------------------------------------
;; beyond primitives
;; ------------------------------------------------
;;
;; So far we've only dealt with functions that interact directly with
;; state. In practice, we want to execute functions that are specific
;; to our domain, that don't interact directly with the flow state
;;
;; Here's a more practical example, in which we draw from state
;; to provide arguments to domain functions.
;; domain fns
(defn new-db [] (atom {:users #{}}))
(defn register-user [db user] (swap! db update :users conj user))
(defn fetch-users [db] (:users @db))
;; helper fns
(defn register-user-helper [user]
(flow "register user"
(flow/get-state (fn [{:keys [db]}] (register-user db user)))))
(defn fetch-users-helper []
(flow "fetch users"
(flow/get-state (fn [{:keys [db]}] (fetch-users db)))))
;; flow
(run
(flow "store and retrieve a user"
(register-user-helper {:name "Phillip"})
(fetch-users-helper))
{:db (new-db)})
;; => [#{{:name "Phillip"}} {:db #<Atom@4d7999d2: {:users #{{:name "Phillip"}}}>}]
;;
;; ------------------------------------------------
;; assertions
;; ------------------------------------------------
;;
;; state-flow includes a wrapper around matcher-combinators to support
;; making assertions.
;;
;; See https://github.com/nubank/matcher-combinators
;; `match?` takes two args:
;; - an expected value, or matcher
;; - an actual value, or a step that will produce one
;;
;; example matching a value
(run
(flow "store and retrieve a user"
(register-user-helper {:name "Phillip"})
[users (fetch-users-helper)]
(flow "user got added"
(match? #{{:name "Phillip"}} ;; << expected value
users))) ;; << actual value
{:db (new-db)})
;; example matching the <left-value> produced by a step
(run
(flow "store and retrieve a user"
(register-user-helper {:name "Phillip"})
(flow "user got added"
(match? #{{:name "Phillip"}} ;; << expected value
(fetch-users-helper)))) ;; << step which produces value
{:db (new-db)})
;; ------------------------------------------------
;; failure semantics
;; ------------------------------------------------
;; When an assertion fails, it prints the failure message to the
;; repl, e.g.
(run
(flow "store and retrieve a user"
(register-user-helper {:name "Phillip"})
(flow "user got added"
(match? #{{:name "Philip"}} ;; <- different spelling
(fetch-users-helper))))
{:db (atom {:users #{}})})
;; => [#{{:name "Phillip"}} {:db #<Atom@4a65a50f: {:users #{{:name "Phillip"}}}>}]
;; --- printed to repl ---
;; FAIL in () (form-init13207122878088623810.clj:256)
;; interact with db (line 270) -> user got added (line 273) -> match? (line 274)
;; expected: (match? #{{:name "Philip"}} actual__12555__auto__)
;; actual: #{{:name (mismatch "Philip" "Phillip")}}
;; --- /printed to repl ---
;; When a flow throws an exception ...
;;
;; `run` returns the exception as a value
(run
(flow "fails"
(match? 2 1)
(flow/invoke #(throw (ex-info "boom!" {})))
(flow "is never run"
(match? 4 3)))
{})
;; `run*` raises the exception by default
(run*
{:init (constantly {})}
(flow "fails"
(match? 2 1)
(flow/invoke #(throw (ex-info "boom!" {})))
(flow "is never run"
(match? 4 3))))
;; ------------------------------------------------
;; clojure.test integration: defflow
;; ------------------------------------------------
(require '[state-flow.api :refer [defflow]])
;; defflow produces a function, like clojure.test's deftest, which
;; can be invoked directly, or through clojure.test's run functions
(defflow store-and-retrieve-users {:init (constantly {:db (atom {:users #{}})})}
(register-user-helper {:name "Phillip"})
(flow "user got added"
(match? #{{:name "Philip"}}
(fetch-users-helper))))
(meta #'store-and-retrieve-users)
;; => {:test #function[user/fn--16196],
;; :line 324,
;; :column 1,
;; :file "/Users/david/work/nubank/state-flow/doc/walkthrough.repl",
;; :name store-and-retrieve-users,
;; :ns #namespace[user]}
(clojure.test/test-vars [#'store-and-retrieve-users])
;; Common practice is to include a project-specific version of
;; defflow, which wraps defflow and passes it a common :init,
;; e.g.
(defmacro custom-defflow [name & flows]
`(defflow ~name {:init (constantly {:db (atom {:users #{}})})
:runner run}
~@flows))
(custom-defflow store-and-retrieve-users-again
(register-user-helper {:name "Phillip"})
(flow "user got added"
(match? #{{:name "Philip"}}
(fetch-users-helper))))
(meta #'store-and-retrieve-users-again)
(clojure.test/test-vars [#'store-and-retrieve-users-again])