Poor Man's Effects?

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.

1 Like

In case the "what" isn't exactly clear from the brief explanation, I'll add some of the definitions to clarify the mechanism.

The constructor for an action, the one you'll typically use when dealing with them, looks like:

;; this happens inside the `define-action` macro
(struct type super (arg ...))

(define (ctor fds ... . args)
  (raise-with-current-effect
   (make-action type 'name fds ... args)))

The operative procedure here being raise-with-current-effect which looks like:

(define (raise-with-current-effect action)
  (with-handlers ([exn:action? {current-effect}])
    (raise action)))

When the action is created, it is immediately raised using the current-effect as the handler.

The programmer then uses the with-control macro to scope this current-effect parameter.

(define (extend-effect effs)
  (define pass (current-effect))
  (lambda (action)
    (with-handlers ([exn:action? pass])
      (let loop ([effs effs])
        (if (null? effs)
            (raise action)
            (with-handlers ([exn:action? (car effs)])
              (loop (cdr effs))))))))

(define-syntax-rule
  (with-control effs . body)
  (with-handlers ([exn:action:return? return-values])
    (parameterize ([current-effect (extend-effect effs)])
      . body)))

As a convenience, there is also the return action for returning completely from the immediate with-control scope.

(define-action return (values ...))

(define (return-values r)
  (apply values (exn:action:return-values r)))

Although the programmer is of course free to override this within the control itself.

1 Like