Skip to content

Commit

Permalink
add walkthrough file
Browse files Browse the repository at this point in the history
  • Loading branch information
dchelimsky committed Jan 20, 2020
1 parent 851620d commit 25abd99
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 1 deletion.
1 change: 1 addition & 0 deletions dev/user.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
(ns user)
262 changes: 262 additions & 0 deletions doc/walkthrough.repl
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
(require '[state-flow.core :refer [flow run]]
'[state-flow.state :as state]
'[state-flow.cljtest :refer [match?]])

;; -------------------------------------------
;; introduction the state monad and primitives
;; -------------------------------------------

;; state-flow is implemented using a state monad. If you're already
;; familiar with monads, great! If not, don't worrry. We'll explain
;; just enough about the state monad to understand state-flow.
;;
;; - A monad is a wrapper around a function.
;; - A state monad is a monad whose function is a function of
;; some mutable state, which is managed for you by a runner.
;;
;; state-flow includes a number of state monad constructors, e.g.

(state/get)
;; => #cats.monad.state.State{:mfn #object[...],
;; :state-context #<State-E>}
;;
;; - :mfn is the wrapped function
;; - :state-context is a reference to mutable state, which will be
;; managed by state-flow.
;;
;; The :mfn of a state monad is a function of state, which returns
;; a Pair of [result-of-invocation, state-after-invocation]
;;
;; The result-of-invocation of the :mfn of the `(state/get)` monad is
;; the value of the state, and, since this fn doesn't modify state, the
;; state-after-invocation is the same.

((:mfn (state/get))
{:count 0})
;; #<Pair [{:count 0} {:count 0}]>
;;
;; state-flow provides a `run` function that handles the unwrapping
;; and manages state for you:

(run (state/get) {:count 0})
;; => #<Pair [{:count 0} {:count 0}]>
;;
;; The `run` function takes a state monad and an (optional) initial
;; state, and returns the Pair returned by invoking the :mfn with
;; the state.
;;
;; We refer to `(state/get)` and some other monad constructors as
;; primitives. Here's another primitive, which returns a specified
;; value without modifying the internal state:

(run (state/return {:count 37}) {:count 0})
;; => #<Pair [{:count 37} {:count 0}]>

;; And we can also return the application of a function to state
;; without modifying the state:

(run (state/gets (fn [s] (update s :count inc))) {:count 0})
;; => #<Pair [{:count 1} {:count 0}]>

;; This next one does the reverse of `state/gets`: it returns the
;; unmodified state and applies a function to the state

(run (state/modify (fn [s] (update s :count inc))) {:count 0})
;; => #<Pair [{:count 0} {:count 1}]>
;;
;; Or we can replace the internal state entirely, returning the previous state:

(run (state/put {:name "Jacob"}) {:count 0})
;; => #<Pair [{:count 0} {:name "Jacob"}]>

;; -------------------------------------------
;; flows
;; -------------------------------------------
;;
;; We use flows to string together several monads in a single
;; monad:

(flow "counter"
(state/modify (fn [s] (update s :count inc)))
(state/modify (fn [s] (update s :count inc)))
(state/modify (fn [s] (update s :count inc))))
;; => #cats.monad.state.State{:mfn #object[...],
;; :state-context #<State-E>}
;;
;; And, then we can hand that directly to the run function:

(run (flow "counter"
(state/modify (fn [s] (update s :count inc)))
(state/modify (fn [s] (update s :count inc)))
(state/modify (fn [s] (update s :count inc))))
{:count 0})
;; => #<Pair [{:count 2} {:count 3}]>
;;
;; `run` returns a Pair of [return state], just as in previous examples.
;; The return of a flow is the return of the last step. Within the flow,
;; each step is run in sequence, passing the initial state to the first
;; step, then the resulting state (the 2nd value in the returned Pair)
;; to the next, and so on, e.g.
;;

;; (run (flow "counter"
;; (state/modify (fn [s] (update s :count inc)))
;; ^^ fn is invoked with {:count 0}, produces #<Pair [{:count 0} {:count 1}]>
;; (state/modify (fn [s] (update s :count inc))) ;; => #<Pair [{:count 1} {:count 2}]>
;; ^^ fn is invoked with {:count 1}, produces #<Pair [{:count 1} {:count 2}]>
;; (state/modify (fn [s] (update s :count inc)))) ;; => #<Pair [{:count 2} {:count 3}]>
;; ^^ fn is invoked with {:count 2}, produces #<Pair [{:count 2} {:count 3}]>
;; {:count 0})
;; => #<Pair [{:count 2} {:count 3}]>
;; ^^ the value produced by the last invocation within the flow

;;
;; The return from running a flow is no different than the return from
;; running a primitive: a Pair of [result state], where result is the
;; result of applying the last monad function in the flow to the state
;; _after_ having applied the previous functions (hence the input)
;;
;; -------------------------------------------
;; programming model
;; -------------------------------------------
;;
;; Since monads are values, we can use all the familiar tools of Clojure
;; to define and compose them.

(def inc-count (state/modify (fn [s] (update s :count inc))))

(def inc-twice
(flow "increment twice"
inc-count
inc-count))

(run inc-twice {:count 0})
;; => #<Pair [{:count 1} {:count 2}]>
;;
;; And we can nest flows arbitrarily deeply!

(run
(flow "inc (parent)"
inc-count
(flow "inc twice (child)"
inc-twice
(flow "inc 2 more times (grandchild)"
inc-twice))
(state/gets (fn [s] (update s :count * 3))))
{:count 0})
;; => #<Pair [{:count 15} {:count 5}]>

;; -----------------------------
;; bindings
;; -----------------------------
;;
;; bindings let you bind the returns of monads to symbols,
;; which are then in scope for the remainder of the flow

(run
(flow "binding example"
[count-before (state/gets :count)] ;; <- binds 0 to `count-before`
inc-count
[count-after (state/gets :count)] ;; <- binds 1 to `count-after`
(state/return {:before count-before
:after count-after}))
{:count 0})
;; => #<Pair [{:before 0, :after 1} {:count 1}]>

;; 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 [start 37]]
(state/modify (fn [s] (update s :count + start)))
(state/gets :count))
{:count 0})
;; => #<Pair [37 {:count 37}]>

;; And those values can come from evaluating regular Clojure expressions:

(run
(flow "binding example"
[:let [start (+ 30 7)]]
(state/modify (fn [s] (update s :count + start)))
(state/gets :count))
{:count 0})
;; => #<Pair [37 {:count 37}]>

;; -----------------------------
;; 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
;; To run a normal clojure expression in a flow, you need to wrap it.

(run (flow "vanilla clojure in a flow" (state/wrap-fn #(+ 1 2))) {})
;; => #<Pair [3 {}]>

;; Here's a more practical example

(defn register-user [db user]
(swap! db update :users conj user))

(defn fetch-users [db]
(:users @db))

(let [db (atom {:names #{}})]
(run
(flow "interact with db"
(state/wrap-fn #(register-user db {:name "Phillip"}))
(state/wrap-fn #(fetch-users db)))))
;; => #<Pair [#{"Phillip"} {}]>

;; ---------------------------
;; assertions
;; ---------------------------

;; state-flow includes a wrapper around matcher-combinators to support
;; making assertions

(let [db (atom {:names #{}})]
(run
(flow "interact with db"
(state/wrap-fn #(register-user db {:name "Phillip"}))
[users (state/wrap-fn #(fetch-users db))]
(match? "user got added"
users
[{:name "Phillipx"}]))))

;; could also be written as

(let [db (atom {:names #{}})]
(run
(flow "interact with db"
(state/wrap-fn #(register-user db {:name "Phillip"}))
(match? "user got added"
(state/wrap-fn #(fetch-users db))
[{:name "Phillip"}]))))

(run (match? "" 1 2))

;; ---------------------------
;; failure semantics
;; ---------------------------
;; A quick example of failing in flow

;; `run` returns the exception as a value
#_(run (flow ""
(match? "fails" 1 2)
(state/wrap-fn #(throw (ex-info "boom!" {})))
(match? "is never run" 3 4))
{})


;; `run!` raises the exception
#_(state-flow.core/run!
(flow ""
(match? "fails" 1 2)
(state/wrap-fn #(throw (ex-info "boom!" {})))
(match? "is never run" 3 4))
{})
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
flow [[:block 1]]}}

:profiles {:uberjar {:aot :all}
:dev {:source-paths ["config"]
:dev {:source-paths ["dev"]
:dependencies [[ns-tracker "0.4.0"]
[org.clojure/tools.namespace "0.3.1"]
[midje "1.9.9"]
Expand Down

0 comments on commit 25abd99

Please sign in to comment.