Hi, Racket Discourse.
I have been toying around with the idea of effect-like controls--in the sense of effect-systems--for use in my experiments with Ollama.
I want a way to constrain when and where tool-calls can be made, but not make a whole ordeal of having to validate the constraints from a programmer's perspective, nor make the plumbing too imposing.
I think effect-systems, and capability-based programming as a concept, are interesting ways of maintaining invariants, which is of particular importance when working with the unsleeping dreamers.
The idea I am working on, is to use exceptions as the primitive for the controls, but focus the "scope" of the exception to the call-site via parameterization.
I call these exceptions "actions", and the procedures for handling them "effects", which are themselves scoped to "controls" to enforce the parameterizations.
So, for example:
(define-action risky-thing (info))
(define-effect decide-risk
[(risky-thing 'trust-me-bro) (empty-the-accounts)])
(with-control [decide-risk]
;; we'll assume an LLM made a tool-call which leads to risky-thing
... (risky-thing risk) ...)
Here, when risky-thing is called, it grabs the current-effect (from the with-control), which then decides how the action is handled. If the action is handled successfully, it returns the values from this result, otherwise it raises the action to the next enclosing control, if any. This may in turn return values, or be handled in some other way, including simply raising the action all the way to the top-level.
What's also neat about using exceptions, is that they carry continuation-marks with them, which can be exposed as extra "context" if necessary:
(define-action collatz (n))
(define-effect collatz-apply
[(collatz 1)
#:with ([start #:default 1]
[steps #:default 0])
`(,steps steps from ,start)]
[(collatz (? even? n))
#:with (start [steps #:default 0]) ;; here we match the incoming context
(collatz (/ n 2)
#:with ([start (or start n)] ;; here we modify the outgoing context
[steps (+ steps 1)]))]
[(collatz (? odd? n))
#:with (start [steps #:default 0])
(collatz (+ (* 3 n) 1)
#:with ([start (or start n)]
[steps (+ steps 1)]))])
(with-control [collatz-apply]
(values
(collatz 1023)
(collatz 61)))
'(62 steps from 1023)
'(19 steps from 61)
Coming from Python way back when, using exceptions for control-flow seems natural, but I am curious what this style of exception-handling is called or would be called, if it is worth a name.