Skip to content

Commit

Permalink
refactor macros: lifting is out, hoisting is in
Browse files Browse the repository at this point in the history
* Instead of lifting intermediate cells inside formulas, references
  to cells are hoisted out and the entire expression wrapped in an
  anon fn and that is lifted.
* Add defc and defc= macros.
* Use @cemerick cljs.test for testing.
* Attempt to fix console logging in phantom and headless ff.
* Rewrite cell= and defc= macros.
* Remove special handling of fn*.
* Avoid creating anonymous cell in set-cell!= because memory leaks.
* Unsupported specials are now down to just ns, def, deftype*, and
  defrecord.
  • Loading branch information
micha committed Oct 16, 2013
1 parent 98c4726 commit cbf9de1
Show file tree
Hide file tree
Showing 8 changed files with 648 additions and 772 deletions.
15 changes: 15 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## 2.0.0-SNAPSHOT

*Wed Oct 16 11:12:01 EDT 2013*

* Use hoisting instead of lifting when constructing formulas.
* Unsupported special forms are now down to just `ns`, `def`, `deftype*`,
and `defrecord*`.
* It is no longer an error to destroy a cell that other cells reference
in their formulas.

*Mon Oct 14 14:08:09 EDT 2013*

* Add `defc` and `defc=` macros.

*Sun Oct 13 18:24:15 EDT 2013*

* Remove `mx` macro.
Expand All @@ -13,6 +25,9 @@
* Add `input?` macro.
* Watches can be added to input and formula cells. IWatchable is now fully
supported again by all Cells.
* Move `tailrecursion.javelin.macros` namespace to `tailrecursion.javelin`.
* Remove `cell?`, `input?`, `cell`, `set-cell!`, and `destroy-cell!`
macros—use the functions with corresponding names instead.

*Tue Oct 8 19:28:37 EDT 2013*

Expand Down
161 changes: 68 additions & 93 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ change._

```clojure
(ns your-ns
(:require tailrecursion.javelin) ;; necessary if compiling in advanced mode
(:require-macros [tailrecursion.javelin.macros :refer [cell cell=]]))
(:require [tailrecursion.javelin :refer [cell]])
(:require-macros [tailrecursion.javelin :refer [cell=]]))

(defn start []
(let [a (cell 0) ;; input cell with initial value of 0.
Expand Down Expand Up @@ -52,87 +52,102 @@ Artifacts are published on [Clojars][3].
## Overview

Javelin provides a spreadsheet-like computing environment consisting
of **cells**, **values**, and **formulas**. Cells are similar to
Clojure atoms: they contain values, they can be dereferenced with
`deref` or the `@` reader macro, and they implement the `IWatchable`
interface. Formulas are ClojureScript expressions that may contain
references to other cells.
of **cells**, **values**, and **formulas**. The `Cell` reference type
is the FRP equivalent of the Clojure atom. Formulas are ClojureScript
expressions that may contain references to other cells.

##### All Cells

* contain values.
* implement the `IWatchable` interface.
* are dereferenced with `deref` or the `@` reader macro.

##### Input Cells

* contain values that are updated explicitly using `reset!` or `swap!`.
* are created by the `cell` macro.
* are created by the `cell` function or `defc` macro.
* are updated explicitly using `swap!` or `reset!`.

##### Formula Cells

* contain values that are recomputed _reactively_ according to a formula.
* are read-only—attempts to update a formula cell directly
via `swap!` or `reset!` results in an error.
* are created by the `cell=` macro.
* 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.

Some examples of cells:

```clojure
(def a (cell 42)) ;; cell containing the number 42
(def b (cell '(+ 1 2))) ;; cell containing the list (+ 1 2)
(def c (cell (+ 1 2))) ;; cell containing the number 3
(def d (cell {:x @a})) ;; cell containing the map {:x 42}

(def e (cell= {:x a})) ;; cell with formula {:x a}, updated when a changes
(def f (cell= (+ a 1))) ;; cell with formula (+ a 1), updated when a changes
(def g (cell= (+ a ~(inc @a)))) ;; cell with formula (+ a 43), updated when a changes
(def h (cell= [e f g])) ;; cell with formula [e f g], updated when e, f,
;; and/or g change

@h ;;=> [{:x 42} 43 85]
(reset! a 7) ;;=> 7
@h ;;=> [{:x 7} 8 50]
(swap! f inc) ;;=> ERROR: f is a formula cell, it updates itself!
(defc a 42) ;; cell containing the number 42
(defc b '(+ 1 2)) ;; cell containing the list (+ 1 2)
(defc c (+ 1 2)) ;; cell containing the number 3
(defc d {:x @a}) ;; cell containing the map {:x 42}

(defc= e {:x a}) ;; cell with formula {:x a}, updated when a changes
(defc= f (+ a 1)) ;; cell with formula (+ a 1), updated when a changes
(defc= g (+ a ~(inc @a))) ;; cell with formula (+ a 43), updated when a changes
(defc= h [e f g]) ;; cell with formula [e f g], updated when e, f, or g change

@h ;;=> [{:x 42} 43 85]
(reset! a 7) ;;=> 7
@h ;;=> [{:x 7} 8 50]
(swap! f inc) ;;=> ERROR: f is a formula cell, it updates itself!
```

Note the use of `~` in the definition of `g`. The expression
`(inc @a)` is evaluated and the resulting value is used when creating
the formula, rather than being recomputed each time the cell updates.
See the [Formulas][9] section below.

Cells can be microbeasts...

```clojure
(defc test-results
{:scores [74 51 97 88 89 91 72 77 69 72 45 63]
:proctor "Mr. Smith"
:subject "Organic Chemistry"
:sequence "CHM2049"})

(defc= test-results-with-mean
(let [scores (:scores test-results)
mean (/ (reduce + scores) (count scores))
grade (cond (<= 90 mean) :A
(<= 80 mean) :B
(<= 70 mean) :C
(<= 60 mean) :D
:else :F)]
(assoc test-results :mean mean :grade grade)))
```

## Formulas

To create a formula cell all macros in the given formula expression
are fully expanded. Then the resulting expression is walked recursively
according to the following rules:

* **Special forms** `if`, `do`, `new`, and `throw` are replaced
with reactive equivalents.
* **Collection literals** are replaced with their sexp equivalents
and then walked.
* **Anonymous function bodies** and **quoted expressions** are not
walked.
* **The unquote form** causes its argument to be evaluated in place
and not walked.
* **The unquote-splicing form** is interpreted as the composition
of `unquote` and `deref`.
of `unquote` as above and `deref`.

Some things don't make sense in formulas and cause errors:

* **Special forms** `def`, `loop*`, `letfn*`, `try*`, `recur`, `ns`,
`deftype*`, `defrecord*`, and `&` are not supported and cause
compile-time exceptions.
* **Circular dependencies** cause infinite loops and stack overflow
errors.
* **Unsupported forms** `def`, `ns`, `deftype*`, and `defrecord*`.
* **Circular dependencies** between cells result in infinite loops at
runtime.

## Javelin API

Requiring the namespace and macros:

```clojure
(ns my-ns
(:require tailrecursion.javelin)
(:require
[tailrecursion.javelin :refer [cell? input? cell set-cell! destroy-cell!]])
(:require-macros
[tailrecursion.javelin.macros
:refer [cell? input? cell cell= set-cell! set-cell!= destroy-cell!]]))
[tailrecursion.javelin :refer [cell= defc defc= set-cell!=]]))
```

Cell macros:
API functions and macros:

```clojure
(cell? c)
Expand All @@ -147,62 +162,22 @@ Cell macros:
(cell= expr)
;; Create new fomula cell with formula expr.

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

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

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

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

(destroy-cell! c)
;; Removes c from the cell graph so it can be GC'd. It's an error
;; to destroy a cell if other cells refer to it in their formulas.
```

## Lisp vs. Spreadsheet Evaluation

The spreadsheet evaluation model is a push-based system&mdash;very
different from the usual, pull-based Lisp evaluation model. In Lisp,
forms are evaluated depth first, and only as needed to produce a
value. This model supports special forms and macros, which decide when
to evaluate their own arguments. In the Javelin evaluation model this
is impossible because formula cells are recomputed _reactively_ based
on the values of the argument cells (cells the formula cell depends
on), which must therefore be computed first.

Consequences of this include:
* "Short-circuiting" expressions (like `and` and `if`, for example)
don't work that way when used in a formula&mdash;all clauses are
always evaluated. The cell's value will be correct (i.e. the cell
will contain the value of the correct clause) but side effects in
all clauses will be performed on every update.
* Macros that expand to expressions containing unsupported special
forms (like `doseq` and `for`, for example, which expand to
expressions containing the unsupported `loop*` form) can't be
used in formulas.

In these cases the solution is to wrap the expression in an anonymous
function to protect it from being lifted when the `cell=` macro walks
the code. The `cell=` macro will not descend into anonymous function
bodies.

For example:

```clojure
(def x (cell 1))
(def y (cell 1))
(def z (cell [1 2 3]))

;; This cell prints both "even" and "odd".
(cell= (if (even? (+ x y)) (.log js/console "even") (.log js/console "odd")))

;; This cell prints only "even" or "odd".
(cell= (#(if (even? (+ %1 %2)) (.log js/console "even") (.log js/console "odd")) x y))

;; This throws a js error because loop* is not supported.
(cell= (doseq [i z] (.log js/console i)))

;; This works as intended because the cell= macro doesn't walk the fn.
(cell= (#(doseq [i %] (.log js/console i)) z))
;; Disconnects c from the propagation graph so it can be GC'd.
```

## License
Expand Down
8 changes: 5 additions & 3 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[tailrecursion/cljs-priority-map "1.0.2"]
[alandipert/desiderata "1.0.1"]]
:plugins [[lein-cljsbuild "0.3.2"]]
:source-paths ["src/clj" "src/cljs"]
:plugins [[lein-cljsbuild "0.3.2"]
[com.cemerick/clojurescript.test "0.1.0"]]
:source-paths ["src"]
:repl-options {:init-ns tailrecursion.javelin}
:cljsbuild {:builds
{:test
{:source-paths ["src/clj" "src/cljs" "test"]
{:source-paths ["src" "test"]
:compiler {:output-to "test/test.js"
:optimizations :advanced
;:optimizations :whitespace
Expand Down
Loading

0 comments on commit cbf9de1

Please sign in to comment.