# cl-reactive **Repository Path**: triobox/cl-reactive ## Basic Information - **Project Name**: cl-reactive - **Description**: Reactive programming at the variable/function level for Common Lisp - **Primary Language**: Unknown - **License**: Unlicense - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-06-05 - **Last Updated**: 2021-06-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # CL-REACTIVE: Reactive functions in Common Lisp The `CL-REACTIVE` package provides support for reactive programming in Common Lisp at the function level. In reactive programming, changing the value of a variable causes other variables to be updated and other functions to be invoked. The common example of reactive programming is a spreadsheet where changing the value of one cell causes a variety of other cells to be recalculated. Common Lisp already has `Cells` which is reactive programming based on CLOS slots. Here, reactive programming is handled at a function and variable level instead of at the slot level. This package provides signal variables and signal functions. Signal variables hold a single, typed value. Signal functions calculate their value from the values of signal variables and/or other signal functions. Signal variables and signal functions are collectively referred to as signals. In most instances, it is not necessary to distinguish between signal variables and signal functions. The only important distinction between the two are that one can explicitly set the signal value of a signal variable while one cannot do the same for a signal function. Constants and functions can be used in place of signals in most places that expect a signal. ## Basic Signal Manipulations The fundamental building blocks for all signal operations are: querying signal values, defining new signal variables, defining new signal functions, and delaying eager recalculation of signal functions. ### Querying Signal Values Given a signal `SIG`, one can query its value with the `SIGNAL-VALUE` method. (signal-value sig) => current value of the signal Calling `SIGNAL-VALUE` with `SIG` calls the function with no arguments and returns that value. Calling `SIGNAL-VALUE` with `SIG` a constant (or anything other than a function or signal) returns that constant. If `SIG` is a signal variable, one can set the signal value with `SETF`: (setf (signal-value sig) new-signal-value) One can also use the convenience macro `WITH-SIGNAL-VALUES` to work directly with the value: (defmacro with-signal-values (&rest decls) &body body) The `DECLS` here is a list of entries each of the form `(VARNAME SIGNAL)`. Each `VARNAME` will be bound to the signal value of the corresponding `SIGNAL`. For example, given signals `SIG1` and `SIG2` where `SIG1` is a signal variable and `SIG2` is any signal, one might do something like: (with-signal-values ((v1 sig1) (v2 sig2) (v3 5)) (incf v1) (+ v1 v2 v3)) The `WITH-SIGNAL-VALUES` macro here uses `SYMBOL-MACROLET` to bind `V1` to the value of `SIG1`, `V2` to the value of `SIG2`, and `V3` to the value of `5`. ### Defining Signal Variables One can define a signal variable with global scope using the macro `DEFSIGNAL-VARIABLE`: (defmacro defsignal-variable (name value &key (type t) documentation) ...) This macro creates a global signal variable bound to the given `NAME`. The initial value of the signal variable is `VALUE`. One can optionally specify the `TYPE` for this signal's values and a `DOCUMENTATION` string for this signal. For example, the following snippet creates a signal variable for monitoring the current temperature: (defsignal-variable *current-temperature* 0 :type real :documentation "Current temperature in degrees Celsius.") One can also define signal variables in the local context using the macro `SIGNAL-LET`: (defmacro signal-let ((&rest decls) &body body) ...) The `SIGNAL-LET` macro acts like `LET` but binds signal variables to the names. The `DECLS` in `SIGNAL-LET` can be either a symbol or a list of the form `(NAME VALUE &KEY (TYPE T) DOCUMENTATION)`. For example, the following creates a list of three signal variables. (signal-let ((sig-a 0 :type integer :documentation "First Signal") (sig-b nil :documentation "Second Signal") sig-c) (list sig-a sig-b sig-c)) ### Defining Signal Functions One can define a signal function (calculated signal) with global scope using the macro `DEFSIGNAL-FUNCTION`: (defmacro defsignal-function (name (&rest depends) &body body) ...) This macro creates a global signal function bound to the given `NAME`. The signal value is calculated from the `BODY` using the signals listed in `DEPENDS`. The `DEPENDS` here is a list of entries each of the form `(VARNAME SIGNAL)`. Each `VARNAME` will be bound to the signal value of the corresponding `SIGNAL` for the `BODY`. The `BODY` will be executed each time one of the `DEPENDS` signals is updated. The `NAME` here can be either a symbol or a list of the form `(NAME &KEY (TYPE T) DOCUMENTATION)`. The results generated by `BODY` must conform to the specified `TYPE`. The `DOCUMENTATION` here will be used on the signal function itself as well as the wrapper function `#'NAME`. If `DOCUMENTATION` is not specified and the `BODY` begins with a string, that string will be used as the `DOCUMENTATION`. For example, the following snippet creates a signal variable for monitoring the current temperature in Farenheit: (defsignal-function (current-temperature-f :type real) ((celsius *current-temperature*)) "Current temperature in degrees Farenheit." (+ (* 9/5 celsius) 32)) One can also define signal functions in the local context using the macro `SIGNAL-FLET`: (defmacro signal-flet ((&rest fdecls) &body body) ...) Each entry in the `FDECLS` list is of the form `(NAME (&REST DEPENDS) &BODY SIG-BODY)`. This macro creates a local signal function bound to the given `NAME`. The signal value is calculated from the `SIG-BODY` using the signals listed in `DEPENDS`. The `DEPENDS` here is a list of entries each of the form `(VARNAME SIGNAL)`. Each `VARNAME` will be bound to the signal value of the corresponding `SIGNAL` for the `SIG-BODY`. The `SIG-BODY` will be executed each time one of the `DEPENDS` signals is updated. The `NAME` here can be either a symbol or a list of the form `(NAME &KEY (TYPE T) DOCUMENTATION)`. The results generated by `BODY` must conform to the specified `TYPE`. The `DOCUMENTATION` here will be used for the signal function. The `SIGNAL-FLET` macro acts like `FLET` but binds signal functions to the names. The signal functions depend on other signals. For example, the following creates a list of two signal functions that both depend on the signal `SIG-X`. (signal-flet ((sig-abs-x ((x sig-x)) (abs x)) ((sig-sqr-x :type integer) ((x sig-x)) (* x x))) (list sig-abs-x sig-sqr-x)) ### Deferring Signal Updates There are times when one will be updating multiple signal variables which are closely related. Some signal functions may well depend upon both of them. For these instances, it is convenient to delay recalculating signals until all of the updates are done. One can use the `WITH-SIGNAL-UPDATES-DEFERRED` macro around a body of code where signal functions will not eagerly update until the macro has completed. Signal functions which are needed within the `WITH-SIGNAL-UPDATES-DEFERRED` section will be calculated as needed. For example, in the following code, the (signal-flet ((sig-min ((mx sig-mouse-x) (my sig-mouse-y)) (min mx my)) (sig-max ((mx sig-mouse-x) (my sig-mouse-y)) (max mx my))) (with-signal-values ((mx sig-mouse-x) (my sig-mouse-y) (mouse-max sig-max)) (with-signal-updates-deferred () (setf mx new-x my new-y) (list (/ mx mouse-max) (/ my mouse-max))))) Neither `SIG-MAX` nor `SIG-MIN` will be updated when `MX` and `MY` are assigned. The `SIG-MAX` signal will be updated when it is used to create the `LIST`. The `SIG-MIN` signal will not be updated until the `WITH-SIGNAL-UPDATES-DEFERRED` section ends. ## Composing Signals The fundamental signal operations above can be composed in a variety of ways to make more powerful signal constructs. This section describes the functions `CL-REACTIVE` provides for composing signals. ### Counting Signal Updates The `SIGNAL-COUNT` function creates a signal function that returns the number of times the signal SIG has been updated. (defun signal-count (sig &key documentation) ...) So, for example, to count the number of updates to `SIG-X` in a given section of code, one might do: (with-signal-values ((count-x (signal-count sig-x))) ... whatever code one wants here ... count-x) Note: This count actually reflects the number of times that a signal function dependent on SIG is updated so if SIG is updated multiple times within a single WITH-SIGNAL-UPDATES-DEFERRED section, this count would likely not reflect all of those updates. ### Selecting Between Signals The `SIGNAL-IF` function takes three arguments: `SIG-COND`, `SIG-TRUE`, and `SIG-FALSE`. It returns a signal function whose value is that of `SIG-TRUE` if `SIG-COND` is non-`NIL` and the value of `SIG-FALSE` otherwise. (defun signal-if (sig-cond sig-true sig-false &key documentation) ...) It is often convenient to use a constant value in place of either `SIG-TRUE` or `SIG-FALSE` (or both): (signal-if sig-cond :yes :no) ### Triggering Only On Changes A signal function will be triggered each time one of the signals it depends upon is changed. Sometimes, the signal function will recalculate its value and come up with the same result it did the last time. With the `SIGNAL-ON-CHANGE` function, one can create a signal that triggers its dependents only when the value of its input signal changes. (defun signal-on-change (sig &key (key #'identity) (test #'equal) documentation) ...) The returned signal only triggers when `SIG` changes value. The given `TEST` is used to tell when two values are the same. The `KEY` function is invoked on the previous value and the current value then the `TEST` function is invoked with the results to determine whether the two values are equal. In the following example, the `SIGNAL-MAX-CHANGED` signal will not trigger its dependents even though the `SIGNAL-MAX` signal will. (signal-let ((sig-x 100 :type integer) (sig-y 100 :type integer)) (signal-flet ((signal-max ((x sig-x) (y sig-y)) (max x y))) (let ((signal-max-changed (signal-on-change signal-max))) (with-signal-values ((x sig-x)) (setf x 50))))) One can poll the `SIGNAL-VALUE` at any time, but other signals which depend upon this one will not be triggered. The `TEST` function is passed the `KEY` of the previous value followed by the `KEY` of the new value of `SIG`. One can use this to create specialized signals. The following example triggers only on positive zero-crossings: (labels ((positive-crossing-p (a b) (and (or (minusp a) (zerop a)) (plusp b))) (not-a-positive-crossing-p (a b) (not (positive-crossing-p a b)))) (signal-on-change sig :test #'not-a-positive-crossing-p)) Note: when the `TEST` returns non-`NIL`, there is no change. As such, the test is the negation of when there is the desired change. Another way one might create an equivalent signal is this: (flet ((plusp-ish (a) (if (plusp a) 1 0))) (signal-on-change sig :key #'plusp-ish :test #'>=)) Again, the `TEST` is used to tell when successive values are equivalent. The `TEST` of `#'>=` is, thus, the opposite of when we want to trigger the signal: `#'<`. ### Signal as a Function of Other Signals The `SIGNAL-APPLY` function creates a signal function whose value is obtained by applying a given function `FN` to the values of a list `SIGNALS` of signals. (defun signal-apply (fn signals &key (type t) documentation) ...) For example, if one needs a signal that tracks the sum of the signals `SIG-A`, `SIG-B`, and `SIG-C`, one can do: (signal-apply #'+ (list sig-a sig-b sig-c)) One can also take advantage of constant signals here to do fancier calculations. This following example creates a signal that returns the position of the last character `A` in the value of `SIG-STRING`: (signal-apply #'position (list #\A sig-string :from-end t)) Of course, one could also have written the previous example like this: (signal-apply (lambda (s) (position #\A s :from-end t)) (list sig-string)) ### Reducing a List of Signals The `SIGNAL-REDUCE` function creates a signal function of whose value is that of `CL:REDUCE` when run on a given list of signals. (defun signal-reduce (fn signals &key (from-end nil from-end-p) (start nil startp) (end nil endp) (initial-value nil initial-value-p) (key nil keyp) (type t) documentation) ...) The `TYPE` parameter gives the type specifier for the resulting signal. The `DOCUMENTATION` is used to document the resulting signal function. The `FROM-END`, `START`, `END`, `INITIAL-VALUE`, and `KEY` parameters are passed directly to `CL:REDUCE`. Note: The `START` and `END` do not make as much sense here as they do in `CL:REDUCE`. Here, if one uses `START` and/or `END` to specify a (proper) sublist of `SIGNALS`, the resulting signal function will depend on signals that are not used in calculating its value. This means that it will be triggered at times when there is no hope that its value will change. It makes more sense to trim the `SIGNALS` list before calling `SIGNAL-REDUCE`. If one has some number of input signals in the list `SIGNALS` and wants a signal which is the product of those signals, then one might do the following: (signal-reduce #'* signals :initial-value 1) ## A Word About Mutability It is perfectly possible to create a signal whose value is a `LIST`: (defsignal-variable *properties* (list :a 1 :b 2 :c 3) :type list :documentation "ABC properties.") The behavior of the system is undefined if the signal value is destructively modified: (nreverse (signal-value *properties*)) ; Bad! Any number of other signals and functions may be looking at that list at any point in time. Additionally, such destructive changes do not trigger signals which depend on the value to be notified that it changed. The `CL-REACTIVE` package makes no attempt to enforce the immutability of the signal values. It is incumbent on those using `CL-REACTIVE` to ensure that they do not mutate any values used as signal values. The behavior of the system is also undefined if any signal variable is rebound. For example: (defsignal-variable *x* 0) ... (setf *x* (signal-let ((new-x 0)) new-x))