Clamps Packages
Defining relations
cl-refs provides two ways to define a relation between ref-objects, or between a ref-object and some program logic, make-computed and watch.
make-computed
This function combines creating a new ref-object with establishing a relation between the created object and one or more other ref-objects. It takes a function as argument. All ref-objects referenced in the body of that function using get-val will cause the newly created ref-object to update its value by calling the function whenever the value of any of these ref-objects is changed. make-computed returns the newly created ref-object.
(defvar c1 (make-ref 1.0)) ; => c1 c1 ; => #<ref 1.0> (defvar c2 (make-computed (lambda () (* 2 (get-val c1))))) c2 ; => #<ref 2.0> (get-val c2) ; => 2.0 (set-val c1 12) ; => 12 (get-val c2) ; => 24 ;;; NOTE: The other direction is undefined: (set-val c2 30) ; => 30 (get-val c1) ; => 12 !!!
Here is an example using two related ref-objects:
(defvar d1 (make-ref 1)) ; => d1 (defvar d2 (make-ref -4)) ; => d2 (defvar d3 (make-computed (lambda () (+ (get-val d1) (get-val d2))))) ; => d3 (get-val d3) ; => -3 (set-val d1 10) ; => 10 (get-val d3) ; => 6 (set-val d2 5) ; => 5 (get-val d3) ; => 15
In case a two-way relation between ref-objects is needed, another function defining the reverse computation can be supplied as optional second argument to make-computed:
(setf c2 (make-computed ;; function called to set c2 ;; whenever any of the ;; contained ref-objects are ;; changed: (lambda () (* 2 (get-val c1))) ;; function called whenever c2 gets changed using ;; (set-val c2 val): (lambda (val) (set-val c1 (/ val 2))))) ; => #<ref 24> c2 ; => #<ref 24> (get-val c2) ; => 24 (set-val c1 7) ; => 7 (get-val c2) ; => 14 ;;; Now the other direction works as well: (set-val c2 30) ; => 30 (get-val c1) ; => 15
watch
Like make-computed also watch takes a function as argument. This function is called, whenever one or more ref-objects referenced in its body using get-val are changed. In that way, actions can be triggered and associated with the change of ref-objects1. Since actions can also involve changing other ref-objects, watch can be used in a similar fashion as make-computed.
Note that the call to watch will trigger the execution of the supplied function once. This is necessary to register the function in the referenced ref-objects and to ensure the correct state of the application in case relations between ref-objects are defined within the function.
watch returns a function to remove the action defined by the supplied function. It is crucial to capture this result in order to be able to later remove the established connections between variables and associated actions2.
Important Note
Calling the same watch expression twice will establish two independent functions which will always be called on change of any contained ref-object. If the result of watch wasn't captured, removing the defined function(s) is only possible by redefining all referenced objects with the result that any other relation previously established using make-computed or watch is referencing outdated ref-objects and will have to get redefined. Therefore it is not only advisable to capture the return value of all calls to watch, but also to put all definitions of ref-objects and their relations into a function or a piece of code reloadable at runtime to be able to reset all relations, preferably with additional code reestablishing a defined application state of all used ref-objects.
(defvar e1 (make-ref 1)) ; => e1 ;; Variable to capture watch definitions: (defvar unwatch nil) ; => unwatch (push (watch (lambda () (format t "e1 has changed to ~a~%" (get-val e1)))) unwatch) (set-val e1 40) ;; => 40 ;; output in the REPL: ;; e1 has changed to 40 unwatch ; => (#<function (lambda () :in watch) {1009EAD9DB}>) ;; define another action to be taken: (push (watch (lambda () (format t "another relation: e1 has changed to ~a~%" (get-val e1)))) unwatch) ;; => (#<function (lambda () :in watch) {100D3F59DB}> ;; #<function (lambda () :in watch) {1009EAD9DB}>) ;; output in the REPL: ;; another relation: e1 has changed to 40 (set-val e1 10) ;; => 10 ;; output in the REPL: ;; another relation: e1 has changed to 10 ;; e1 has changed to 10 ;; clear all connections by calling the functions returned by the call ;; to #'watch: (mapc #'funcall unwatch) (setf unwatch nil) (set-val e1 17) ;; => 17 ;; No further output in the REPL.
Footnotes:
In Lisp parlance this is the classic example of a side-effect.
For people used to patcher based systems like Pure Data or Max/MSP, watch serves a similar purpose as patch cords in these systems. Calling the function returned by watch in that context is similar to removing a patch cord, although the analogy shouldn't be overstressed considering the significant differences between a graph-based message-passing paradigm in these systems and the structural layout of cl-ref/clamps.