Tweaks mostly.

This commit is contained in:
mike-thompson-day8 2015-02-22 22:28:40 +11:00
parent 61e2964c6a
commit a9c8d76e1e
6 changed files with 69 additions and 64 deletions

View File

@ -15,6 +15,7 @@ re-frame is still Alpha. But getting closer.
Be sure you use v0.5.0 of reagent.
Todo:
- provide a hook for logging and exception handling
- implement pure event handlers. A macro will be needed.
<!--
@ -27,11 +28,13 @@ alt="Leiningen logo" title="The man himself" align="right" />
Either:
1. You want to develop an app in ClojureScript, and you are looking for a framework; or
2. You believe that ReactJS decisively won the javascript framework wars in early 2015 and
you are wondering how the combination of
"reactive programming", "functional programming" and "immutable data" could
**change everything**. Could this be a case of:
"The future is already here - it's just not very evenly distributed".
2. You believe that by early 2015 ReactJS had effectively won the javascript framework wars and
you are curious about the bigger implications. Could it be that the combination of
`reactive programming`, `functional programming` and `immutable data`
**will completely change everything**? If so, what does that look like in a language
that naturally embodies those paradigms?
> The future is already here - it's just not very evenly distributed.
## re-frame
@ -60,7 +63,7 @@ Features:
5. The surprising thing about re-frame is how simple it is. Beautifully simple! Our reference
implementation is little more than 100 lines of (clojurescript) code. Learn it in an afternoon.
6. But it scales up so nicely to more complex apps. Frameworks are just pesky overhead at small
scale - its how they help you to tame complexity that defines them.
scale - its how they help you to tame the complexity of bigger apps that measure them.
7. Re-frame is impressively buzzword compliant: it has FRP-nature,
unidirectional data flow, pristinely pure functions, conveyor belts, statechart-friendliness (FSM)
and claims an immaculate hammock conception.
@ -71,7 +74,8 @@ __Warning__: this is a long document. That was the summary.
## What Problem Does It Solve?
First we decided to build apps with ClojureScript, then we choose [Reagent], then we had a problem.
First we decided to build our SPA apps with ClojureScript, then we
choose [Reagent], then we had a problem.
For all its considerable brilliance, Reagent (+ ReactJS)
delivers only the 'V' part of a traditional MVC framework.
@ -97,10 +101,10 @@ and there's clearly a 'V' bit and there's a layer which is
Yes, that's true. But to quote McCoy: "It's MVC, Jim,
but not as we know it".
In re-frame none of the M, V, or C bits are objects, they
In re-frame, none of the M, V, or C bits are objects, they
are pure functions (or pure data), and
and they are all wired together via reactive data flows. It is just such a sufficiently different
version of (traditional, smalltalk) MVC that calling it MVC would likely just be confusing. I'd
and they are all wired together via reactive data flows. It is sufficiently different in nature
from (traditional, smalltalk) MVC that calling it MVC would likely just be confusing. I'd
love an alternative.
Perhaps it is a RACES framework - Reactive-Atom Component Event
@ -117,12 +121,9 @@ insider's joke, conference T-Shirt.
Not much about re-frame is original or clever. You'll find
no ingenious use of functional zippers, transducers or core.async.
Re-frame does use Reagent's features in a novel way.
And we did actively reject
the current ClojureScript fashion of using Cursors (which turned
out to be a terrific decision).
But apart from that, for the most part, re-frame is a mashup of
Re-frame does use Reagent's features in a slightly novel way.
And we did actively reject the current ClojureScript fashion of using Cursors.
But, for the most part, re-frame is just a mashup of
emerging ideas.
(For the record,
@ -147,7 +148,6 @@ At small scale, any framework or architecture seems like pesky overhead. The
explanatory examples in here are necessarily small scale, so you'll need to
squint a little to see the benefits that accrue at larger scale.
## Guiding Philosophy
__First__, above all we believe in the one true [Dan Holmsand], creator of Reagent, and
@ -163,9 +163,10 @@ But you'll only really "get"
Reagent when you view it as an FRP-ish library. To put that another way, we think
that Reagent, at its best, is closer in nature to [Hoplon] or [Elm] than it is OM.
__Finally__, we believe in one-way data flow. No two way data binding. We don't like read/write `cursors` which
promote the two way flow of data. As programs get bigger, we've found that their use seems to
encourage control logic into all the wrong places.
__Finally__, we believe in one-way data flow. No two way data binding. We don't
like read/write `cursors` which
promote the two way flow of data. As programs get bigger, we've found that their
use seems to encourage control logic into all the wrong places.
## FRP Clarifications
@ -276,7 +277,7 @@ this is also very easy. Saving past states is trivial, and you will automaticall
good sharing guarantees to keep the size of the snapshots down.
To this list of benefits, I would briefly add two: the ability to genuinely model control via FSMs
To this list, I would briefly add two: the ability to genuinely model control via FSMs
and the ability to do time travel debugging, even in a production setting. More on both soon.
[Hoplon] takes the same approach via what they called `stem cells`, which is a root source of data.
@ -305,13 +306,14 @@ Richard Dawkins
### How Flow Happens In Reagent
To implement FRP, Reagent provides a `ratom` and a `reaction`. re-frame uses both of these
building blocks, so let's now make sure we understand them before going further.
To implement FRP, Reagent provides a `ratom` and a `reaction`.
re-frame uses both of these
building blocks, so let's now make sure we understand them.
`ratoms` behave just like normal ClojureScript atoms. You can `swap!` and `reset!` them, `watch` them, etc.
From a ClojureScript perspective, the purpose of an atom is to hold mutable data. From a re-frame
perspective, we'll tweak that paradigm ever so slightly and **view a `ratom` as being a value that
perspective, we'll tweak that paradigm ever so slightly and **view a `ratom` as having a value that
changes over time.** Seems like a subtle distinction, I know, but because of it, re-frame sees a
`ratom` as a Signal. [Pause and read this](http://elm-lang.org/learn/What-is-FRP.elm).
@ -755,8 +757,8 @@ that it chains `reactions`:
```
The original version had only one `reaction` which would be re-run completely each time `app-db` changed.
The new version, has chained reactions.
The 1st and 2nd reactions just extract from `db`. They will fire each time `db-app` changes.
This new version, has chained reactions.
The 1st and 2nd reactions just extract from `db`. They will run each time `db-app` changes.
But they are cheap. The 3rd one does the expensive
computation using the result from the first two.
@ -910,6 +912,7 @@ They `dispatch` pure data and nothing more.
Collectively, event handlers provide the control logic in a re-frame application.
An event handler is a pure function of two parameters:
1. current value in `app-db`. Note: that's the map **in** `app-db`, not the atom itself.
2 an event (represented as a vector)
@ -946,6 +949,7 @@ app-db --> components --> Hiccup --> Reagent --> VDOM --> React -->
```
The `router` will:
1. inspect the 1st element of the arriving vector
2. look in its registry for the handler which is registered for this kind of event
3. call that handler with two parameters: (1) the current value in `app-db` and (2) the event vector
@ -1102,7 +1106,7 @@ modify `app-db` themselves. That is always done in a handler.
## The CPU Hog Problem
Sometime a handler has a lot of CPU intensive work to do, and it takes a while to get through.
Sometimes a handler has a lot of CPU intensive work to do, and getting through it will take a while.
When a handler hogs the CPU, nothing else can happen. Browsers only give us one thread of
execution and that CPU-hogging handler owns it, and it isn't giving it up. The UI will be

View File

@ -11,8 +11,7 @@
:profiles {:debug {:debug true}
:dev {:dependencies [[spellhouse/clairvoyant "0.0-48-gf5e59d3"]]
:plugins [[lein-cljsbuild "1.0.4"]
#_[com.cemerick/clojurescript.test "0.3.3" ]]}}
:plugins [[lein-cljsbuild "1.0.4"]]}}
:clean-targets [:target-path
@ -28,8 +27,7 @@
;; - lein install
;; :jar-exclusions [#"(?:^|\/)re-frame-demo\/"]
:cljsbuild {:builds [;; currently bogus, there is no demo
{:id "demo"
:cljsbuild {:builds [{:id "demo" ;; currently bogus, there is no demo or tests
:source-paths ["src"]
:compiler {:output-to "run/compiled/demo.js"
:source-map "run/compiled/demo.js.map"

View File

@ -2,7 +2,8 @@
(:require [reagent.core :as r]))
;; The application state
;; Should not be accessed directly by application code (neither handlers or subscriptions)
;; Should never be accessed directly by application code
;; Access is mediated via handlers and subscriptions
(def app-db (r/atom {}))

View File

@ -3,16 +3,11 @@
(:require-macros [cljs.core.async.macros :refer [go-loop go]])
(:require [reagent.core :refer [flush]]
[re-frame.db :refer [app-db]]
[re-frame.utils :refer [first-in-vector]]
[re-frame.utils :refer [first-in-vector warn]]
[cljs.core.async :refer [chan put! <! timeout]]))
(defn warn
[& args]
(.warn js/console (apply str args)))
;; -- register of handlers ------------------------------------------------------------------------
;; -- register of event handlers ------------------------------------------------------------------------
(def ^:private id->fn (atom {}))
@ -20,20 +15,18 @@
"register a handler for an event"
[event-id handler-fn]
(when (contains? @id->fn event-id)
(warn "Overwriting an event-handler" event-id)) ;; allow it, but warn.
(warn "re-frame: overwriting an event-handler" event-id)) ;; allow it, but warn.
(swap! id->fn assoc event-id handler-fn))
;; -- The Event Conveyor Belt --------------------------------------------------------------------
;; A channel which moves events from dispatch to handlers.
;;
;; 1. "dispatch" puts events onto this chan, and
;; 2. "router" reads from the chan, and calls associated handlers
;; This enables async handling of events -- which is a good thing.
(def ^:private event-chan (chan))
;; Moves events from "dispatch" to the router loop.
;; Key architecutal purpose is to cause aysnc handling of events.
(def ^:private event-chan (chan)) ;; TODO: how big should we make the buffer?
;; -- router --------------------------------------------------------------------------------------
;; -- lookup and call -----------------------------------------------------------------------------
(defn- handle
"Look up the handler for the given event, then call it, passing in 2 parameters."
@ -41,20 +34,22 @@
(let [event-id (first-in-vector event-v)
handler-fn (get @id->fn event-id)]
(if (nil? handler-fn)
(warn "No event handler registered for event: " event-id )
(warn "re-frame: no event handler registered for: \"" event-id "\". Ignoring.") ;; TODO: make exception
(handler-fn app-db event-v))))
;; In a loop, read events from the dispatch channel, and then call the
;; right handler.
;; -- router loop ---------------------------------------------------------------------------------
;;
;; In a loop, read events from the dispatch channel, and route them
;; to the right handler.
;;
;; Because handlers occupy the CPU, before each event is handled, hand
;; back control to the GUI render process, via a (<! (timeout 0)) call.
;; back control to the browser, via a (<! (timeout 0)) call.
;;
;; In odd cases, we need to pause for an entire annimationFrame, to ensure that
;; the DOM is fully flushed, before calling a handler known to hog the CPU
;; for an extended period. In that case the event should have metadata
;; Example:
;; the DOM is fully flushed, before thencalling a handler known to hog the CPU
;; for an extended period. In such a case, the event should have metadata
;; Example usage:
;; (dispatch ^:flush-dom [:event-id other params])
;;
;; router loop
@ -67,7 +62,7 @@
(recur)))
;; -- helper --------------------------------------------------------------------------------------
;; -- dispatch ------------------------------------------------------------------------------------
(defn dispatch
"reagent components use this function to send events.
@ -75,13 +70,13 @@
(dispatch [:delete-item 42])"
[event-v]
(if (nil? event-v)
(warn "dispatch is ignoring a nil event.") ;; nil would close the channel
(warn "re-frame: \"dispatch\" is ignoring a nil event.") ;; nil would close the channel
(put! event-chan event-v)))
;; TODO: remove sync handling. I don't like it, even for testing.
(defn dispatch-sync
"sync version of above that actually does the dispatch"
"Invoke the event handler sycronously, avoiding the async-inducing use of core.async/chan"
[event-v]
(handle event-v))

View File

@ -1,18 +1,18 @@
(ns re-frame.subs
(:require
[re-frame.db :refer [app-db]]
[re-frame.utils :refer [first-in-vector]]))
[re-frame.utils :refer [first-in-vector warn]]))
;; mappings from handler-id to handler-fn
;; maps from handler-id to handler-fn
(def ^:private key->fn (atom {}))
(defn register
"register a function for a handler id"
"register a hander function for an id"
[key-v handler-fn]
(if (contains? @key->fn key-v)
(println "Warning: overwritting a subscription-handler: " key-v)) ;; TODO: more generic logging
(warn "re-frame: overwriting subscription-handler for: " key-v)) ;; allow it, but warn.
(swap! key->fn assoc key-v handler-fn))
@ -21,5 +21,6 @@
[v]
(let [key-v (first-in-vector v)
handler-fn (get @key->fn key-v)]
(assert (not (nil? handler-fn)) (str "No subscription handler registered for key: " key-v))
(handler-fn app-db v)))
(if (nil? handler-fn)
(warn "re-frame: no subscription handler registered for: \"" key-v "\". Returning nil.")
(handler-fn app-db v))))

View File

@ -1,7 +1,13 @@
(ns re-frame.utils)
(defn warn
[& args]
(.warn js/console (apply str args)))
(defn first-in-vector
[v]
(assert (vector? v) (str "Expected a vector event, but got: " v))
(first v))
(if (vector? v)
(first v)
(warn "re-frame: expected a vector event, but got: " v)))