|(require kinda-ferpy)||package: kinda-ferpy|
This section provides a walkthrough. If you already understand the basics, skip to the Reference.
To create a cell, wrap stateful-cell around a single Racket value.
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
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.
(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.
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.
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.
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:
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.
(stateful-cell maybe-dependency ... body ...+)
| #:dependency existing-cell-id
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.
(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)))))
(make-stateful-cell [ #:dependencies explicit-dependencies] managed) → stateful-cell? explicit-dependencies : (listof stateful-cell?) = '() managed : (if/c procedure? (-> any/c) any/c)
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:
If explicit-dependencies is an empty list, then make-stateful-cell assumes that you would rather let it discover dependencies for you. In this case, managed will be applied in a parameterization where discovery-phase? is #t. Any other stateful cells applied in the body of the procedure bound to managed will be captured as dependencies of the stateful cell holding managed. managed is then applied once more (where discovery-phase? is #f) to store its initial value. Any stateful cells that are not evaluated in the body of managed are not captured as dependencies.
If explicit-dependencies is not empty, then make-stateful-cell assumes that you know what you want and no discovery phase is necessary. managed will be applied once (with discovery-phase? set to #f) to initialize its cell value. explicit-dependencies are used as-is to construct relationships to other cells.
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.
(make-stateful-cell/async [ #:dependencies explicit-dependencies] managed) → stateful-cell? explicit-dependencies : (listof stateful-cell?) = '() managed : (-> any/c)
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))
(stateful-cell (when (thread? (current-cell-value) (kill-thread (current-cell-value)))) (thread ...))
For the rest, there’s rename-in.
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))))))