;;; Copyright © 2024 David Thompson ;;; Modifications Copyright © 2024 Vivianne Langdon ;;; ;;; This program is free software: you can redistribute it and/or ;;; modify it under the terms of the GNU General Public License as ;;; published by the Free Software Foundation, either version 3 of the ;;; License, or (at your option) any later version. ;;; ;;; This program is distributed in the hope that it will be useful, ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;;; General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with this program. If not, see ;;; . (use-modules (ice-9 match) (srfi srfi-9) (srfi srfi-9 gnu) ((hoot hashtables) #:select (make-weak-key-hashtable weak-key-hashtable-ref weak-key-hashtable-set!)) ((hoot lists) #:select (fold)) ((hoot numbers) #:select (truncate)) (hoot ffi)) (define-foreign timeout "window" "setTimeout" (ref extern) f64 -> i32) (define-foreign document-body "document" "body" -> (ref null extern)) (define-foreign make-text-node "document" "makeTextNode" (ref string) -> (ref extern)) (define-foreign make-element "document" "makeElement" (ref string) -> (ref extern)) (define-foreign append-child! "element" "appendChild" (ref extern) (ref extern) -> (ref extern)) (define-foreign attribute-set! "element" "setAttribute" (ref extern) (ref string) (ref string) -> none) (define-foreign value "element" "getValue" (ref extern) -> (ref string)) (define-foreign set-value! "element" "setValue" (ref extern) (ref string) -> none) (define-foreign add-event-listener! "element" "addEventListener" (ref extern) (ref string) (ref extern) -> none) (define-foreign replace-with! "element" "replaceWith" (ref extern) (ref extern) -> none) (define (lset-adjoin = list . rest) (define pred (if (or (eq? = eq?) (eq? = eqv?)) = (lambda (x y) (= y x)))) (let lp ((ans list) (rest rest)) (match rest (() ans) ((x . rest*) (lp (if (member x ans pred) ans (cons x ans)) rest*))))) (define (any pred lst) (let lp ((lst lst)) (match lst (() #f) ((x . rest) (or (pred x) (lp rest)))))) (define (every pred lst) (let lp ((lst lst)) (match lst (() #t) ((x . rest) (and (pred x) (lp rest)))))) (define procedure->external* (let ((cache (make-weak-key-hashtable))) (lambda (proc) (or (weak-key-hashtable-ref cache proc) (let ((extern (procedure->external proc))) (weak-key-hashtable-set! cache proc extern) extern))))) (define (queue-task! thunk) (timeout (procedure->external* thunk) 0.0)) (define-record-type (make-nothing) %nothing?) (define (print-nothing nothing port) (display "#" port)) (set-record-type-printer! print-nothing) (define nothing (make-nothing)) (define (nothing? x) (eq? x nothing)) (define-record-type (make-contradiction details) contradiction? (details contradiction-details)) (define (print-contradiction contradiction port) (format port "#" (contradiction-details contradiction))) (set-record-type-printer! print-contradiction) (define contradiction (make-contradiction nothing)) (define-record-type (%make-relations name parent children) relations? (name relations-name) (parent relations-parent) (children relations-children set-relations-children!)) (define (print-relations relations port) (match relations (($ name parent children) (format port "#" name parent children)))) (set-record-type-printer! print-relations) (define current-parent (make-parameter #f)) (define (make-relations name) (%make-relations name (current-parent) '())) (define (add-child! parent child) (when parent (set-relations-children! parent (cons child (relations-children parent))))) (define-record-type (%make-cell relations neighbors content strongest equivalent? merge find-strongest handle-contradiction) cell? (relations cell-relations) (neighbors cell-neighbors set-cell-neighbors!) (content cell-content set-cell-content!) (strongest cell-strongest set-cell-strongest!) ;; Dispatch table: (equivalent? cell-equivalent?) (merge cell-merge) (find-strongest cell-find-strongest) (handle-contradiction cell-handle-contradiction)) (define (print-cell cell port) (match cell (($ ($ name) _ _ strongest) (display "#" port)))) (set-record-type-printer! print-cell) (define-record-type (%make-propagator relations inputs outputs activate) propagator? (relations propagator-relations) (inputs propagator-inputs) (outputs propagator-outputs) (activate propagator-activate)) (define (print-propagator propagator port) (match propagator (($ ($ name) inputs outputs) (display "# " port) (display outputs port) (display ">" port)))) (set-record-type-printer! print-propagator) (define default-equivalent? equal?) ;; But what about partial information??? (define (default-merge old new) new) (define (default-find-strongest content) content) (define (default-handle-contradiction cell) (values)) (define* (make-cell name #:key (equivalent? default-equivalent?) (merge default-merge) (find-strongest default-find-strongest) (handle-contradiction default-handle-contradiction)) (let ((cell (%make-cell (make-relations name) '() nothing nothing equivalent? merge find-strongest handle-contradiction))) (add-child! (current-parent) cell) cell)) (define (cell-name cell) (relations-name (cell-relations cell))) (define (add-cell-neighbor! cell neighbor) (set-cell-neighbors! cell (lset-adjoin eq? (cell-neighbors cell) neighbor))) (define (add-cell-content! cell new) (match cell (($ _ neighbors content strongest equivalent? merge find-strongest handle-contradiction) (let ((content* (merge content new))) (set-cell-content! cell content*) (let ((strongest* (find-strongest content*))) (cond ;; New strongest value is equivalent to the old one. No need ;; to alert propagators. ((equivalent? strongest strongest*) (set-cell-strongest! cell strongest*)) ;; Uh oh, a contradiction! Call handler. ((contradiction? strongest*) (set-cell-strongest! cell strongest*) (handle-contradiction cell)) ;; Strongest value has changed. Alert the propagators! (else (set-cell-strongest! cell strongest*) (for-each alert-propagator! neighbors)))))))) (define (alert-propagator! propagator) (queue-task! (propagator-activate propagator))) (define (make-propagator name inputs outputs activate) (let ((propagator (%make-propagator (make-relations name) inputs outputs activate))) (add-child! (current-parent) propagator) (for-each (lambda (cell) (add-cell-neighbor! cell propagator)) inputs) (alert-propagator! propagator) propagator)) (define (unusable-value? x) (or (nothing? x) (contradiction? x))) (define (primitive-propagator name f) (match-lambda* ((inputs ... output) (define (activate) (let ((args (map cell-strongest inputs))) (unless (any unusable-value? args) (add-cell-content! output (apply f args))))) (make-propagator name inputs (list output) activate)))) (define (compound-propagator name inputs outputs build) (let ((built? #f)) (define (maybe-build) (unless (or built? (and (not (null? inputs)) (every unusable-value? (map cell-strongest inputs)))) (parameterize ((current-parent (propagator-relations propagator))) (build) (set! built? #t)))) (define propagator (make-propagator name inputs outputs maybe-build)) propagator)) (define (constraint-propagator name cells build) (compound-propagator name cells cells build)) (define-record-type (%make-reactive-id clock) reactive-id? (clock reactive-id-clock set-reactive-id-clock!)) (define (make-reactive-id) (%make-reactive-id 0)) (define (reactive-id-tick! id) (let ((t (1+ (reactive-id-clock id)))) (set-reactive-id-clock! id t) `((,id . ,t)))) ;; Partial value structure for FRP (define-record-type (make-ephemeral value timestamps) ephemeral? (value ephemeral-value) ;; Association list mapping identity -> time (timestamps ephemeral-timestamps)) (define (ephemeral-fresher? a b) (let ((b-inputs (ephemeral-timestamps b))) (let lp ((a-inputs (ephemeral-timestamps a))) (match a-inputs (() #t) (((key . a-time) . rest) (match (assq-ref b-inputs key) (#f (lp rest)) (b-time (and (> a-time b-time) (lp rest))))))))) (define (merge-ephemeral-timestamps ephemerals) (define (adjoin-keys alist keys) (fold (lambda (key+value keys) (match key+value ((key . _) (lset-adjoin eq? keys key)))) keys alist)) (define (check-timestamps id) (let lp ((ephemerals ephemerals) (t #f)) (match ephemerals (() t) ((($ _ timestamps) . rest) (match (assq-ref timestamps id) ;; No timestamp for this id in this ephemeral. Continue. (#f (lp rest t)) (t* (if t ;; If timestamps don't match then we have a mix of ;; fresh and stale values, so return #f. Otherwise, ;; continue. (and (= t t*) (lp rest t)) ;; Initialize timestamp and continue. (lp rest t*)))))))) ;; Build a set of all reactive identifiers across all ephemerals. (let ((ids (fold (lambda (ephemeral ids) (adjoin-keys (ephemeral-timestamps ephemeral) ids)) '() ephemerals))) (let lp ((ids ids) (timestamps '())) (match ids (() timestamps) ((id . rest) ;; Check for consistent timestamps. If they are consistent ;; then add it to the alist and continue. Otherwise, return ;; #f. (let ((t (check-timestamps id))) (and t (lp rest (cons (cons id t) timestamps))))))))) (define (merge-ephemerals old new) (cond ((nothing? old) new) ((nothing? new) old) ((ephemeral-fresher? new old) new) (else old))) (define (ephemeral-wrap proc) (match-lambda* ((and ephemerals (($ args) ...)) (match (merge-ephemeral-timestamps ephemerals) (#f nothing) (timestamps (make-ephemeral (apply proc args) timestamps)))))) (define-record-type (%make-transient cell id) transient? (cell transient-cell) (id transient-id)) (define* (make-transient cell #:optional (id (make-reactive-id))) (%make-transient cell id)) (define (set-trans! transient value) (match transient (($ cell id) (let ((timestamp (reactive-id-tick! id))) (add-cell-content! cell (make-ephemeral value timestamp)))))) (define (get-trans transient) (ephemeral-value (cell-content (transient-cell transient)))) (define* (primitive-reactive-propagator name proc) (primitive-propagator name (ephemeral-wrap proc))) (define-syntax-rule (define-primitive name proc) (define name (primitive-reactive-propagator 'name proc))) (define-syntax-rule (define-constraint (name cells ...) body ...) (define (name cells ...) (constraint-propagator 'name (list cells ...) (lambda () body ...)))) (define-syntax thru (syntax-rules (->) ((thru (inputs ... -> output) body ...) (begin ((primitive-reactive-propagator 'anonymous-propa (lambda (inputs ...) body ...)) inputs ... output) output)) ((thru (inputs ...) body ...) (let ((output (make-cell 'anonymous-cell))) ((primitive-reactive-propagator 'anonymous-propa (lambda (inputs ...) body ...)) inputs ... output) output)))) (define (r:attribute input elem attr) (let ((attr (symbol->string attr))) (define (activate) (match (cell-strongest input) (($ val) (attribute-set! elem attr (obj->string val))) ;; Ignore unusable values. (_ (values)))) (make-propagator 'r:attribute (list input) '() activate))) (define-record-type (make-binding trans default numeric? group) binding? (trans binding-trans) (default binding-default) (numeric? binding-numeric?) (group binding-group)) (define* (binding trans #:key (default nothing) (numeric? #f) (group '())) (make-binding trans default numeric? group)) (define (obj->string obj) (if (string? obj) obj (call-with-output-string (lambda (port) (write obj port))))) (define (string->obj str) (call-with-input-string str read)) (define* (r:binding binding elem) (match binding (($ ($ cell id) default numeric? group) (define (update new) (unless (nothing? new) (let* ((timestamp (reactive-id-tick! id))) (add-cell-content! cell (make-ephemeral new timestamp)) ;; Freshen timestamps for all cells in the same group. (for-each (lambda (other) (unless (eq? other cell) (match (cell-strongest other) (($ val) (add-cell-content! other (make-ephemeral val timestamp))) (_ #f)))) group)))) ;; Sync the element's value with the cell's value. (define (activate) (match (cell-strongest cell) (($ val) (set-value! elem (if numeric? (object->string val) val))) (_ (values)))) ;; Initialize element value with the default value. (update default) ;; Sync the cell's value with the element's value. (add-event-listener! elem "input" (procedure->external (lambda (event) (update (if numeric? (string->number (value elem)) (value elem)))))) (make-propagator 'r:binding (list cell) '() activate)))) (define (cell->elem cell) (let ((exp (cell-strongest cell))) (if (unusable-value? exp) (make-text-node "") (sxml->dom exp)))) (define (sxml->dom exp) (match exp ;; The simple case: a string representing a text node. ((? string? str) (make-text-node str)) ((? number? num) (make-text-node (number->string num))) ;; A cell containing SXML (or nothing) ((? cell? cell) (let ((elem (cell->elem cell))) (r:dom cell elem) elem)) ;; An element tree. The first item is the HTML tag. (((? symbol? tag) . body) ;; Create a new element with the given tag. (let ((elem (make-element (symbol->string tag)))) (define (add-children children) ;; Recursively call sxml->dom for each child node and ;; append it to elem. (for-each (lambda (child) (append-child! elem (sxml->dom child))) children)) (match body ((('@ . attrs) . children) (for-each (lambda (attr) (match attr (((? symbol? name) (? string? val)) (attribute-set! elem (symbol->string name) val)) (((? symbol? name) (? number? val)) (attribute-set! elem (symbol->string name) (number->string val))) (((? symbol? name) (? cell? cell)) (r:attribute cell elem name)) ;; The value attribute is special and can be ;; used to setup a 2-way data binding. (('value (? binding? binding)) (r:binding binding elem)) (('event (? string? event-name) (? procedure? proc)) (add-event-listener! elem event-name (procedure->external proc))))) attrs) (add-children children)) (children (add-children children))) elem)))) (define (r:dom input elem) (define (activate) (match (cell-strongest input) (($ exp) (let ((new (sxml->dom exp))) (replace-with! elem new) (set! elem new))) (_ (values)))) (make-propagator 'dom (list input) '() activate)) ;; (define-primitive r:hsv->rgb hsv->rgb) ;;(define-constraint (r:components<->rgb r g b rgb) ;; (r:rgb-color r g b rgb) ;; (r:rgb-color-r rgb r) ;; (r:rgb-color-g rgb g) ;; (r:rgb-color-b rgb b)) (define (render exp) (append-child! (document-body) (sxml->dom exp))) (define-syntax-rule (with-cells (name ...) body . body*) (let ((name (make-cell 'name #:merge merge-ephemerals)) ...) body . body*)) (define (note-component active) (with-cells (enabled) (define t:enabled (make-transient enabled)) (set-trans! t:enabled #f) (define (on-click ev) (set-trans! t:enabled (not (get-trans t:enabled)))) `(div (@ (class "note") (event "click" ,on-click)) ,(thru (active) (if active "I'm active!" "not active")) ,(thru (enabled) (if enabled "I'm enabled!" "not enabled"))))) (with-cells (first-name last-name full-name note-active) (define name (make-reactive-id)) (define name-group (list first-name last-name)) (define note (note-component note-active)) (thru (first-name last-name -> full-name) (string-append first-name " " last-name)) (render `(div (h1 "Propagators Playground") (input (@ (type "text") (value ,(binding (make-transient first-name name) #:default "vivi" #:group name-group)))) (input (@ (type "text") (value ,(binding (make-transient last-name name) #:default "langdon" #:group name-group)))) (h2 ,full-name) ,note)))