Skip to content
ulPublic

About

Spreadsheet FRP library

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

Carbon.RX

Spreadsheet-like FRP for Clojure and ClojureScript.

Clojars Project

Carbon.RX is a love child of Javelin and freactive.core. Cells hold state; reactive expressions derive values from cells and from each other. Dependencies are discovered dynamically every time an expression runs, changes propagate glitch-free, and nothing that is no longer observed is kept alive by the graph.

Installation

;; deps.edn
carbon/rx {:mvn/version "0.4.0-SNAPSHOT"}

;; project.clj
[carbon/rx "0.4.0-SNAPSHOT"]

Quick start

(ns example
  (:refer-clojure :exclude [dosync]) ; Clojure only, when referring dosync
  (:require [carbon.rx :as rx :refer [cell rx lens dosync no-rx]]))

(def price (cell 10))
(def qty   (cell 3))
(def total (rx (* @price @qty)))

@total                     ;=> 30

(add-watch total :log (fn [_ _ old new] (println old "->" new)))

(reset! qty 4)             ; prints 30 -> 40

(dosync                    ; one batch: the watch fires once
  (reset! price 20)
  (reset! qty 5))          ; prints 40 -> 100

The macros are available both from carbon.rx and from carbon.macros.

API

Form What it does
(cell x & {:keys [meta validator]}) A mutable source, used like an atom (deref, reset!, swap!, compare-and-set!, swap-vals!, reset-vals! on the JVM, watches, validators). Alias: $.
(rx & body) A lazy reactive expression. Every cell or expression dereferenced while body runs becomes a dependency. Alias: $$.
(lens getter setter & [meta validator drop-fns]) A writable expression: reads like rx, reset!/swap! call (setter new-value).
(rx/cursor parent path) A lens onto (get-in @parent path) that writes back with assoc-in (or reset! for an empty path). The same parent and path return the same cursor while it is reachable.
(dosync & body) Batch writes. Dependents update and watches fire once, after the outermost dosync.
(no-rx & body) Dereference without creating dependencies.
rx/*value* Inside an expression body: its previous value (:carbon.rx/thunk the first time).
(rx/add-drop x key f) / (rx/remove-drop x key) Call (f key x) when the expression stops being observed.
(rx/gc x) Drop the cached value of an unobserved expression.
rx/cell*, rx/rx*, rx/dosync* Function versions of the macros.

Semantics

Lazy. An expression runs on its first deref, not when it is created.

Observed vs. unobserved. An expression is observed when it has a watch, or when an observed expression depends on it. Only observed expressions are subscribed to their sources. They are updated eagerly after each write, and their watches fire when their value changes (compared with =).

Unobserved expressions are not subscribed to anything, so a long-lived cell never retains them. They still cache their value: a later deref recomputes only if one of their own dependencies has changed. When an expression stops being observed (its last watch is removed, or the expression depending on it switches to another branch), it unsubscribes from its sources and its drop handlers run.

Glitch-free. A write marks everything downstream as possibly stale. Reading an expression first brings its dependencies up to date, so an expression never sees a mix of old and new values. That holds while dependencies change, inside dosync, and inside watch callbacks.

Batching. Inside dosync, writes to cells take effect immediately, and reads (including of expressions) see them. Watches are deferred to the end of the outermost batch and coalesced: one call per reference, from its value before the batch to its value after it. An expression whose value ends up unchanged doesn't notify. Cells notify on every batch that wrote them, like atoms. Writes made by watch callbacks start a new round, and a feedback loop that never settles is reported as an error rather than looping forever.

Exceptions. If dosync's body throws, writes already made are kept and still propagated before the exception is rethrown. If an expression throws, it keeps its previous value and its dependencies. It also starts depending on whatever it read before failing. Reading it again rethrows, and any change to those dependencies retries it. Errors from other expressions and from watch callbacks don't stop the rest of a flush; the first one is rethrown at the end.

Cycles. An expression that, directly or indirectly, reads itself throws ex-info ("carbon.rx: detected a cycle in computation graph!") with the chain of expressions involved.

Rules for expression bodies. Bodies should be pure functions of what they dereference. Writing to cells from inside a body is not supported.

Threads (JVM). Graph operations are serialized by a global lock, so cells and expressions may be used from several threads, and swap! on cells and lenses is atomic. Watches and drop handlers run while that lock is held, so they must not block on other threads that use carbon.rx. Reading a cell outside of an expression doesn't take the lock.

Metadata. The macros attach the source position of the form, and its source text: always on the JVM, in goog.DEBUG builds only on ClojureScript. Pass :meta (any expression) to add your own.

Development

lein test        # JVM tests
npm test         # ClojureScript tests on Node
npm run test:release   # ClojureScript tests, advanced compilation
lein with-profile +bench run   # JVM benchmarks
npm run bench    # Node benchmarks

nix-shell provides Leiningen, a JDK and Node.

Contribution

... is welcomed and very much appreciated! Feel free to ping me with questions and to make PRs.

License

Copyright © 2015–2026 Ruslan Prokopchuk

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

About

Spreadsheet FRP library

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages