From 25abd997faff35db098d934f0158613130d17def Mon Sep 17 00:00:00 2001 From: David Chelimsky Date: Mon, 20 Jan 2020 08:08:25 -0600 Subject: [PATCH] add walkthrough file --- dev/user.clj | 1 + doc/walkthrough.repl | 262 +++++++++++++++++++++++++++++++++++++++++++ project.clj | 2 +- 3 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 dev/user.clj create mode 100644 doc/walkthrough.repl diff --git a/dev/user.clj b/dev/user.clj new file mode 100644 index 0000000..7a96237 --- /dev/null +++ b/dev/user.clj @@ -0,0 +1 @@ +(ns user) diff --git a/doc/walkthrough.repl b/doc/walkthrough.repl new file mode 100644 index 0000000..5aa84e5 --- /dev/null +++ b/doc/walkthrough.repl @@ -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 #} +;; +;; - :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}) +;; # +;; +;; state-flow provides a `run` function that handles the unwrapping +;; and manages state for you: + +(run (state/get) {: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}) +;; => # + +;; 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}) +;; => # + +;; 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}) +;; => # +;; +;; Or we can replace the internal state entirely, returning the previous state: + +(run (state/put {:name "Jacob"}) {:count 0}) +;; => # + +;; ------------------------------------------- +;; 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 #} +;; +;; 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}) +;; => # +;; +;; `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 # +;; (state/modify (fn [s] (update s :count inc))) ;; => # +;; ^^ fn is invoked with {:count 1}, produces # +;; (state/modify (fn [s] (update s :count inc)))) ;; => # +;; ^^ fn is invoked with {:count 2}, produces # +;; {:count 0}) +;; => # +;; ^^ 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}) +;; => # +;; +;; 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}) +;; => # + +;; ----------------------------- +;; 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}) +;; => # + +;; 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}) +;; => # + +;; 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}) +;; => # + +;; ----------------------------- +;; 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))) {}) +;; => # + +;; 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))))) +;; => # + +;; --------------------------- +;; 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)) + {}) diff --git a/project.clj b/project.clj index 6de1fea..56da422 100644 --- a/project.clj +++ b/project.clj @@ -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"]