Skip to content

Commit

Permalink
3.6.0 -- Create lenses via cell= and defc= macros
Browse files Browse the repository at this point in the history
- The cell= and defc= macros now accept an additional argument, a
  callback function. When the callback is provided these macros now
  create lens cells.
  • Loading branch information
micha committed Aug 8, 2014
1 parent 2d653f3 commit 6b0518d
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 39 deletions.
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# javelin

## 3.6.0

*Fri Aug 8 13:15:23 EDT 2014*

* Add optional additional argument to `cell=` and `defc=` macros. When called
with this additional argument they return lens cells.

## 3.5.0

*Thu Aug 7 18:45:31 EDT 2014*
Expand Down
71 changes: 52 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ reference type to represent both.
* are created by the `cell=` or `defc=` macros.
* are updated _reactively_ according to a formula.
* are read-only—updating a formula cell via `swap!` or `reset!`
is an error.
is an error (unless it's a [lens](#lenses)).

Some examples of cells:

Expand Down Expand Up @@ -176,15 +176,15 @@ multiple times.
## Lenses

Lenses separate reads and writes in the cell graph. They are formula cells
created with a special callback that provides the write implementation. The
read implementation is provided by the underlying cell formula.
created with an extra argument—a special callback that provides the
write implementation. The read implementation is provided by the underlying
cell formula.

For example:

```clojure
(defn path-cell [c path]
(lens (cell= (get-in c path))
(partial swap! c assoc-in path)))
(cell= (get-in c path) (partial swap! c assoc-in path)))

(defc a {:a [1 2 3], :b [4 5 6]})
(def b (path-cell a [:a]))
Expand All @@ -200,15 +200,41 @@ For example:
@a ;=> {:a :x, :b [4 5 6]}

@b ;=> :x
(swap! a assoc :a [1 2]) ;=> {:a [1 2] :b [4 5 6]}
(swap! a assoc :a [1 2]) ;=> {:a [1 2], :b [4 5 6]}
@b ;=> [1 2]
```

The `path-cell` function returns a lens whose formula "focuses" in on a part
of the underlying collection using `get-in`. The provided callback takes the
desired new value and updates the underlying collection accordingly. The update
propagates to the lens formula, thereby updating the value of the lens cell
itself.
The `path-cell` function returns a converging lens whose formula focuses in
on a part of the underlying collection using `get-in`. The provided callback
takes the desired new value and updates the underlying collection accordingly
using `assoc-in`. The update propagates to the lens formula, thereby updating
the value of the lens cell itself.

Interestingly, transactions can be used to create diverging lenses, inverting
the above relationship between lens and underlying collection. Instead of
focusing the lens on a single collection to extract a part it, the lens can be
directed toward a number of individual cells to combine them into a single
collection.

For example:

```clojure
(defc a 100)
(defc b 200)
(defc= c {:a a, :b b} #(dosync (reset! a (:a %)) (reset! b (:b %))))

@a ;=> 100
@c ;=> {:a 100, :b 200}
(swap! c assoc :a 200) ;=> {:a 200, :b 200}
@a ;=> 200
```

The `c` lens encapsulates the machinery of atomically updating both `a` and
`b` in the standard cell interface.

Converging and diverging lenses can be useful for low-impact, surgical
refactoring. They encapsulate the value and mutation semantic, eliminating
the need to modify existing code that references the underlying cells.

## Javelin API

Expand Down Expand Up @@ -245,27 +271,34 @@ API functions and macros:
(cell= expr)
;; Create new fomula cell with formula expr.

(lens c f)
;; Creates a "lens" from a cell c and an update callback f. Lenses are formula
;; cells on which swap! or reset! may be called, firing the update callback.
;; The callback must be a function of one argument–the requested new value. The
;; lens will always return the value of the underlying formula cell when it is
;; dereferenced.
(cell= expr f)
;; Create new lens cell with formula expr and callback f. When swap! or reset!
;; is applied to the cell the callback is fired with the requested new value
;; provided as an argument. The callback does not manipulate the lens cell's
;; value directly, but it can swap! or reset! input cells (or do anything else),
;; possibly resulting in a new value being computed by the lens formula.

(defc symbol doc-string? expr)
;; Creates a new input cell and binds it to a var with the name symbol and
;; the docstring doc-string if provided.

(defc= symbol doc-string? expr)
;; Creates a new formula cell and binds it to a var with the name symbol and
;; the docstring doc-string if provided.
;; Creates a new formula cell with formula expr and binds it to a var with the
;; name symbol and the docstring doc-string if provided.

(defc= symbol doc-string? expr f)
;; Creates a new lens cell with formula expr and callback f, and binds it to a
;; var with the name symbol and the docstring doc-string if provided.

(set-cell! c expr)
;; Convert c to input cell (if necessary) with value expr.

(set-cell!= c expr)
;; Convert c to formula cell (if necessary) with formula expr.

(set-cell!= c expr f)
;; Convert c to lens cell (if necessary) with formula expr and callback f.

(destroy-cell! c)
;; Disconnects c from the propagation graph so it can be GC'd.

Expand Down
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
(defproject tailrecursion/javelin "3.5.0"
(defproject tailrecursion/javelin "3.6.0"
:description "A Functional Reactive Programming library for ClojureScript"
:url "https://github.com/tailrecursion/javelin"
:license {:name "Eclipse Public License"
Expand Down
31 changes: 25 additions & 6 deletions src/tailrecursion/javelin.clj
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,31 @@
(let [[f args] (hoist x env)]
(list set-frm' c f args)))

(defmacro cell= [expr] (cell* expr &env))
(defmacro set-cell!= [c expr] (set-cell* c expr &env))
(defmacro defc ([sym expr] `(def ~sym (~cell' ~expr)))
([sym doc expr] `(def ~sym ~doc (~cell' ~expr))))
(defmacro defc= ([sym expr] `(def ~sym (cell= ~expr)))
([sym doc expr] `(def ~sym ~doc (cell= ~expr))))
(defmacro cell=
([expr] (cell* expr &env))
([expr f]
`(with-let [c# (cell= ~expr)]
(set! (.-update c#) ~f))))

(defmacro set-cell!=
([c expr] (set-cell* c expr &env))
([c expr f]
`(with-let [c# c]
(set-cell!= ~c ~expr)
(set! (.-update c#) ~f))))

(defmacro defc
([sym expr] `(def ~sym (~cell' ~expr)))
([sym doc expr] `(def ~sym ~doc (~cell' ~expr))))

(defmacro defc=
([sym expr] `(def ~sym (cell= ~expr)))
([sym doc & [expr f]]
(let [doc? (string? doc)
doc (when doc? [doc])
expr (if doc? expr doc)
f (when-let [f' (if doc? f expr)] [f'])]
`(def ~sym ~@doc (cell= ~expr ~@f)))))

(defmacro cell-let-1 [[bindings c] & body]
(let [syms (bind-syms bindings)
Expand Down
31 changes: 18 additions & 13 deletions test/tailrecursion/javelin/test.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@
(is (= @b :oops)))))

(deftest misc-tests
(testing "lenses work correctly"
(testing "lenses work correctly using lens function"
(let [a (cell 100)
b (cell 200)
c (cell= (+ a b))
Expand All @@ -517,10 +517,20 @@
(is (= @d 400))
(swap! d inc)
(is (= @d 601))))
(testing "lenses work correctly using cell= macro"
(let [a (cell 100)
b (cell 200)
d (cell= (+ a b) (partial reset! a))]
(is (not (input? d)))
(is (= @d 300))
(reset! d 200)
(is (= @d 400))
(swap! d inc)
(is (= @d 601))))
(testing "lens path-cell example works"
(let [a (cell {:a [1 2 3] :b [4 5 6]})
path-cell #(lens
(cell= (get-in %1 %2))
path-cell #(cell=
(get-in %1 %2)
(partial swap! %1 assoc-in %2))
b (path-cell a [:a])]
(is (= @b [1 2 3]))
Expand All @@ -533,15 +543,13 @@
(testing "swap! or reset! on lens returns new value"
(let [a (cell 100)
b (cell 200)
c (cell= (+ a b))
d (lens c (partial reset! a))]
d (cell= (+ a b) (partial reset! a))]
(is (= (swap! d inc) 501))
(is (= (reset! d 100) 300))))
(testing "lenses propagate correctly"
(let [a (cell 100)
b (cell 200)
c (cell= (+ a b))
d (lens c #(reset! a %))
d (cell= (+ a b) #(reset! a %))
e (cell= (+ d a b))]
(is (= @e 600))
(reset! d 200)
Expand All @@ -550,22 +558,19 @@
(let [u (atom [])
a (cell 100)
b (cell 200)
c (cell= (+ a b))
d (lens c #(reset! a %))]
d (cell= (+ a b) #(reset! a %))]
(add-watch d (gensym) #(swap! u conj {:old %3 :new %4}))
(reset! d 200)
(is (= @u [{:old 300 :new 400}]))))
(testing "lenses work correctly in dosync"
(let [u (atom [])
a (cell 100)
b (cell 200)
c (cell= (+ a b))
d (lens c (partial reset! a))
d (cell= (+ a b) (partial reset! a))
e (cell= (swap! u conj d))
u' (atom [])
a' (cell 100)
c' (cell= (+ a' b))
d' (lens c' (partial reset! a'))
d' (cell= (+ a' b) (partial reset! a'))
e' (cell= (swap! u' conj d'))]
(dosync
(is (= @d (swap! d inc)))
Expand Down

0 comments on commit 6b0518d

Please sign in to comment.