Expressive Functional Reactive Programming (Kinda)
1 Guide
1.1 Reading and Writing Values
1.2 Computing Dependent Values
1.3 Cell Lifecycle
1.4 Explicit vs. Implicit Dependencies
1.5 Synchronization
2 Reference
stateful-cell
make-stateful-cell
make-stateful-cell/  async
not-in-cell
current-cell-value
%
stateful-cell?
stateful-cell-dependencies
stateful-cell-dependents
discovery-phase?
7.7

Expressive Functional Reactive Programming (Kinda)

Sage Gerard

 (require kinda-ferpy) package: kinda-ferpy

This module provides a convenient way to write programs using a spreadsheet metaphor. The underlying model is based on the PureState JavaScript library.

1 Guide

This section provides a walkthrough. If you already understand the basics, skip to the Reference.

1.1 Reading and Writing Values

To create a cell, wrap stateful-cell around a single Racket value.

(define x (stateful-cell 1))

To get a value in a cell, apply it to no arguments.

(x) ; 1

To set a new value, apply the cell to that value.

(x 2) ; 2

1.2 Computing Dependent Values

If a cell changes, then the cells that depend on that cell also change. If you’ve used Excel, then you know how this works.

Here, stateful-cell represents optionally-dependent computations. The 1s and (+ (x) (y)) each act as a stateful cell body.

(require kinda-ferpy)
 
(define x (stateful-cell 1))
(define y (stateful-cell 1))
(define sum (stateful-cell (+ (x) (y)))) ; *
 
(displayln (sum)) ; 2
(y 8) ; *
(displayln (sum)) ; 9

Normally Racket would evaluate (+ (x) (y)) every time you apply sum. But here, (+ (x) (y)) actually runs on the lines marked with *. As already stated, (sum) merely retrieves a value that was already computed. sum depends on x and y by virtue of use, and kinda-ferpy will keep all cells in sync for you. This is an example of reactive programming.

If it was not already obvious, this will change how you write code. For example, you might find reason to represent errors as values without raising an exception. A spreadsheet application handles a division by zero by showing a special error value instead of crashing.

Unlike other libraries and languages like FrTime, signals and events are not explicitly declared in kinda-ferpy. So this is not a full interface for reactive programming, it’s just a nice way to model dependency relationships using procedures. That way, when I say "evaluating the cell body" or "applying the cell", I mean the same thing. However, a cell’s behavior is not fully equivalent to a procedure because the expression (+ (x) (y)) does not always run when you apply sum. To understand why this is, we need to cover a cell’s lifecycle.

1.3 Cell Lifecycle

When you create a stateful cell, it starts life with no dependencies and a value of undefined. The cell then goes through a discovery phase to find dependencies. When evaluating expressions during this phase, I’ll say that they do so at discovery time. You can opt-out of this phase by using explicit dependencies as defined in Explicit vs. Implicit Dependencies.

kinda-ferpy evaluates a cell body once if carrying out a discovery phase. Whether it does this or not, it will still evaluate the cell body to compute its initial value. Meaning that by the time a stateful-cell is done evaluating, the cell body ran either once or twice.

1.4 Explicit vs. Implicit Dependencies

A cell like (stateful-cell (+ (x) (y))) uses implicit dependencies, which are stateful cells encountered while evaluating the body of another cell at discovery time. There are blind spots that can later result in incorrect values:

(define switch (stateful-cell #t))
(define x (stateful-cell 1))
(define y (stateful-cell -1))
(define sum (stateful-cell (+ (x) (if (switch) 1 (y)))))
 
(sum) ; 2
(switch #f)
(sum) ; 0
(y 0) ; DANGER: Won't update sum's cell!
(sum) ; Still 0. Should be 1.

From the way if works, (y) is not evaluated at discovery time. It will not be recognized as a dependency of sum. If you want to leverage the discovery phase to find all dependencies, then you need to move dependencies that might not be evaluated out of the if.

(define switch (stateful-cell #t))
(define x (stateful-cell 1))
(define y (stateful-cell -1))
(define sum
  (stateful-cell
    (let ([x-val (x)] [y-val (y)])
      (+ x-val (if (switch) 1 y-val)))))
 
(sum) ; 2
(switch #f)
(sum) ; 0
(y 0) ; Will update sum now
(sum) ; 1

If that seems like a bad precedent to you, then you can list explicit dependencies for your cells using the #:dependency keyword. Doing so will skip the discovery phase for the corresponding cell.

(define switch (stateful-cell #t))
(define x (stateful-cell 1))
(define y (stateful-cell -1))
(define sum
  (stateful-cell
    #:dependency x
    #:dependency y
    #:dependency switch
    (+ (x) (if (switch) 1 (y)))))
 
(sum) ; 2
(switch #f)
(sum) ; 0
(y 0) ; Will update sum
(sum) ; 1

Take care to list every dependency when using explicit dependencies. If you forget to list y as a dependency you’ll still produce incorrect data.

If you don’t want to use explicit dependencies and want to respond to the discovery phase itself, then you can check (discovery-phase?) within a stateful cell body. It will tell you if the cell is being evaluated at discovery time. This gives you a hybrid approach where you can list dependencies for discovery, and express a relatively expensive computation for the times you need to compute a value.

(define c
  (stateful-cell
    (if (discovery-phase?)
        (begin (file-path)
               (file-proc))
        (call-with-input-file (file-path)
                              (file-proc)))))

The value you return in a cell body in a discovery phase ((file-proc), in this case), won’t matter because it won’t be stored as the value of the cell. Exercise caution with additional side-effects at discovery time, because encountering dependencies is the intended side-effect.

1.5 Synchronization

Remember that propogation to all affected cells occurs on a single thread. Multi-threaded applications must treat cells as a shared resource to avoid propogating conflicting data. The below example is equivalent to one hundred threads competing over the current output port:

(define data-cell
  (stateful-cell 0))
(define print-cell
  (stateful-cell (printf "~a " (data-cell))))
 
(void
 (map (λ (i) (thread (λ () (data-cell i))))
      (range 100)))

On a lighter note, change cannot propogate between disconnected cells. One thread may safely read up-to-date information from cells that won’t be affected by another thread.

(define a (stateful-cell 1))
(define b (stateful-cell (a)))
...
(define p (stateful-cell (o)))
 
(define th (thread (λ () (a 2))))
 
(define q (stateful-cell 1))
(define r (stateful-cell (q)))
...
(define z (stateful-cell (y)))
(z)

Cells a through p have no connection to cells q through z. Change happens to propogate safely so long as no two threads try to write to connected cells.

But that’s just it: It happens to be okay. That’s a pitiful standard for engineering, so we need a way to leverage threads for cells when it matters.

We’ll use make-stateful-cell/async to create an asynchronous cell. An asynchronous (or "async") cell applies a procedure of your choice immediately, without blocking. You can apply the async cell to wait for the value of that procedure later.

Explicit dependencies are necessary here because a discovery phase will not find them in the body of a new thread.

(define %file-path (stateful-cell (build-path "my-file")))
(define %file-content-read
  (make-stateful-cell/async #:dependencies (list %file-path)
     (λ () (file->string (%file-path)))))

When you are ready to wait for the file contents, do this:

(define reader (%file-content-read))
(define file-value (reader))

What about exceptions? If the procedure you use in an async cell raises an exception, it will be caught and re-raised at the time you wait for the value.

(define reader (%file-content-read))
(define file-value (with-handlers ([exn:fail? exn-message]) (reader)))

Every cell that depends on asynchronous I/O should assume that the value won’t be immediately available. Let’s say we write a dependent cell that immediately blocks to wait for content:

(define %content
  (stateful-cell
   (define content ((%file-content-read)))
   (string-append "Got from file: " content)))

That just defeats the purpose. It should look like this:

(define %content-modifier
  (stateful-cell
   (define reader (%file-content-read))
   (λ ()
     (define content (reader))
     (string-append "Got from file: " content))))

%content-modifier depends on %file-content-read, but does not block waiting for the file’s contents. Once something finally applies the procedure that waits for values, then it will get the latest content.

2 Reference

syntax

(stateful-cell maybe-dependency ... body ...+)

 
maybe-dependency = 
  | #:dependency existing-cell-id
Creates a stateful cell. The body is placed inside of a new procedure as-is. If no dependencies are explicitly defined using #:dependency, then the procedure containing body will evaluate immediately to discover dependencies. Any stateful cell accessed in body will be flagged as a dependency of the containing cell. body will then be evaluated again to compute the initial value of the cell.

If at least one dependency is defined using #:dependency, the discovery phase will simply use the dependencies you provide instead of evaluating body to discover cells. In this case, body will only be used to compute the value of the cell. Each existing-cell-id is an identifier bound to another cell.

stateful-cell is a macro that expands to an application of make-stateful-cell. See make-stateful-cell for more details.

(define first-operand (stateful-cell 1))
(define second-operand (stateful-cell 2))
(define sum
  (stateful-cell #:dependency first-operand
                 #:dependency second-operand
                 (+ (first-operand) (second-operand))))
 
(define square (stateful-cell (* (sum) (sum))))

For comparison, here’s an equivalent code block that shows expansions of stateful-cell.

(define first-operand (make-stateful-cell (lambda () 1)))
(define second-operand (make-stateful-cell (lambda () 2)))
(define sum
  (make-stateful-cell #:dependencies (list first-operand second-operand)
    (lambda ()
      (let ([a (first-operand)]
            [b (second-operand)]
           (+ a b))))))
 
(define square (make-stateful-cell (lambda () (* (sum) (sum)))))

procedure

(make-stateful-cell [#:dependencies explicit-dependencies] 
  managed) 
  stateful-cell?
  explicit-dependencies : (listof stateful-cell?) = '()
  managed : (if/c procedure? (-> any/c) any/c)
This is a procedure form for stateful-cell. It returns a stateful cell P that, when applied, returns the latest correct version of a Racket value in terms of dependencies.

The behavior of P and make-stateful-cell both depend on managed and explicit-dependencies.

If managed is not a procedure, then (P) will return managed.

If managed is a procedure, then make-stateful-cell will immediately apply managed once or twice according to the value of explicit-dependencies:

Given the above, (P) will return a cached reference to the value last returned from managed.

(P new-managed) will update the stored value of P, and will synchronously update all dependent cells. Be warned that setting new-managed to a different procedure will NOT initialize a new dependency discovery phase, nor will it change the existing dependency relationships of P. If you want to express new dependency relationships, then create a new cell.

procedure

(make-stateful-cell/async 
  [#:dependencies explicit-dependencies] 
  managed) 
  stateful-cell?
  explicit-dependencies : (listof stateful-cell?) = '()
  managed : (-> any/c)
Like make-stateful-cell, with a few differences.

This actually creates two cells. You just get one of them. The other is kept private.

The private cell applies managed immediately in a new thread T, and uses that thread as its value. Whenever a dependency in explicit-dependencies changes, the private cell will apply (thread-break T) and apply managed in a new thread.

A dependency discovery pass will not detect any cells in the body of managed, so you must leverage explicit-dependencies to capture changes relevant to managed.

The cell returned to you depends on the private cell. The returned cell’s value is a procedure R that, when applied, waits for the private cell’s thread to terminate and then returns the value of managed. If managed raises an exception, then (R) will raise that exception.

(define %file-path (stateful-cell (build-path "my-file")))
(define %file-content-read
  (make-stateful-cell/async #:dependencies (list %file-path)
     (λ () (file->string (%file-path)))))
 
(define get-the-value (%file-content-read))
(with-handlers ([exn:fail:filesystem?
                 (printf "Could not read ~a~n" (%file-path))])
  (get-the-value))

(current-cell-value) is the value of a cell when control is in that cell’s body. Use this to clean up or make decisions based on old values.

(stateful-cell
  (when (thread? (current-cell-value)
    (kill-thread (current-cell-value))))
  (thread ...))

If (eq? (current-cell-value) not-in-cell), then control is not in a cell body.

For those who love single-character aliases and reconfiguring their editor.

value

% : stateful-cell

For those who love single-character aliases but hate it when people make them reconfigure their editor.

For the rest, there’s rename-in.

procedure

(stateful-cell? v)  boolean?

  v : any/c
Return #t if v is a value constructed with stateful-cell or make-stateful-cell.

procedure

(stateful-cell-dependencies cell)  (listof stateful-cell?)

  cell : stateful-cell?
Returns a list of cell’s dependencies.

procedure

(stateful-cell-dependents cell)  (listof stateful-cell?)

  cell : stateful-cell?
Returns a list of cell’s dependents.

procedure

(discovery-phase?)  boolean?

Returns #t if the library is currently looking for implicit dependencies.

You can use this to avoid potentially expensive operations within the body of a stateful cell body. If you do not use the #:dependencies argument in stateful-cell, you can use the following pattern to make dependencies visible and avoid unnecessary work.

(define c
  (stateful-cell
    (if (discovery-phase?)
        (begin (file-path)
               (file-proc)
               (void))
        (call-with-input-file (file-path)
                              (unbox (file-proc))))))