Capturing the Dynamic Environment

Previously in A Functional Implementation of Dynamic Scope I noted that when a continuation is invoked, the dynamic environment is reset to be what it was at the time the continuation was captured.

This suggests it might be possible to capture the dynamic environment: to be able to call a function f not with the current dynamic environment (which is what usually happens when we call a function), but with a dynamic environment captured earlier.

Why might that be useful? Consider for example a typical pattern of registering a callback JavaScript:

  fetch(url, function (err, result) {
    if (err) {
      reportError(err);
      return;
    }
    ... do something with result...
  });

Why does the callback have to have the err parameter? Why isn’t it possible for errors to be propagated to an enclosing exception handler?

  try {
    fetch(url, function (result) {...});
  } catch (err) {
    reportError(err);
  }

The fetch function registers the callback and returns. Execution passes out of the try...catch and eventually returns to the event loop. Later the callback gets called (when the fetch operation has succeeded or failed), but by then the try...catch is no longer active. In JavaScript there’s no way to capture the dynamic environment of the exception handler to have it apply later.

We have to add a lot of extra code to ensure errors are handled in all cases. This is helped some with promises, but it’s still a pain.

So let me try creating a capture-dyn that when passed a function, returns a function that when called, calls the original function with the dynamic environment captured at the time capture-dyn execution (instead of the dynamic environment of the caller).

  (let a (parameter)
    (let df (parameterize a 1
              (capture-dyn (fn ()
                             (prn (a)))))
      (parameterize a 2
        (df))))

Without the capture-dyn this would print 2, what a is set to from the caller; but with the capture-dyn it should print 1, what a was set to when the capture-dyn was called.

capture-dyn will take one argument, the function which gets called with the captured dynamic environment:

  (def capture-dyn (f)
    ...)

We want to capture the continuation where the next thing that happens is calling f, since that’s the action we want to have happen when the function returned by capture-dyn is called. For example, if I used a do:

  (def capture-dyn (f)
    ...
    (do (ccc ...)
        (f)))

Invoking the continuation captured by the ccc will in effect return from the ccc form, and so when the continuation is invoked the next thing that happens is that f gets called. Since that’s the continuation to call f, I’ll call it cf:

  (def capture-dyn (f)
    ...
    (do (ccc (fn (cf) ...))
        (f)))

If I were to return from the ccc function, that would also return from the ccc and then call f, which I don’t want to do. I don’t want to call f now while executing capture-dyn, I only want to call it later. So I need some way to avoid returning from the ccc function. I can use catch ... throw to tunnel the continuation out to me:

  (def capture-dyn (f)
    (let cf (catch (do (ccc (fn (cf) (throw cf)))
                       (f)))
      ...))

I want capture-dyn to return a function that when called, invokes the continuation.

  (def capture-dyn (f)
    (let cf (catch (do (ccc (fn (cf) (throw cf)))
                       (f)))
      (fn ()
        (cf nil))))

So far so good. I can now call the function returned by capture-dyn, and it will call f with the captured dynamic environment.

However, it won’t return! Invoking a continuation is like a “goto”, it’s one-way. After the call to (f) execution continues from that point, and so capture-dyn returns again.

To go back to returning from the function, I’ll need a continuation for the point where the returned function is returning. I call that cr, for the return continuation.

  (def capture-dyn (f)
    (let cf (catch (do (ccc (fn (cf) (throw cf)))
                       (f)))
      (fn ()
        (ccc (fn (cr) ...)))))

The second ccc captures the continuation where the next thing to do is to return from the function. If cr is invoked with a value, that value will in effect be returned by the second ccc form, and thus returned from the function.

So if I call f, and pass its return value to cr, that will then return the value returned by f from the function I’m returning from capture-dyn.

  (def capture-dyn (f)
    (let cf (catch ...get cr somehow...
                   (do (ccc (fn (cf) (throw cf)))
                       (cr (f))))
      (fn ()
        (ccc (fn (cr) ...)))))

How can I get cr up into the first part? I have cf, and I’ll be invoking it to call f. Before I just did (cf nil). If I invoke cf with a value, that value will in effect be returned by the first ccc form. So I can pass in the return continuation cr, and grab it as it gets returned by the first ccc.

  (def capture-dyn (f)
    (let cf (catch (let cr (ccc (fn (cf) (throw cf)))
                     (cr (f))))
      (fn ()
        (ccc (fn (cr)
               (cf cr))))))

Aha! It works!

I can tighten this up by noting that (fn (x) (foo x)) is the same as just foo. Thus,

  (def capture-dyn (f)
    (let cf (catch (let cr (ccc throw)
                     (cr (f))))
      (fn ()
        (ccc cf))))

When I first wrote this function, I originally had

  (def capture-dyn (f)
    (let cf (catch ((ccc throw) (f)))  ; dangerous
      (fn ()
        (ccc cf))))

But I realized this was assuming left-to-right evaluation order of function calls. (I was assuming that first (ccc throw) was going to be executed, and then (f)). It’s safer to make the execution order explicit.

I admit I find the definition inscrutable. I’d be happy to find a way to break it down into pieces that were easier to understand. But if not, maybe it’s just one of those things where you need to follow the logic through to see what’s happening.



Home
@awwx
github/awwx
andrew.wilcox@gmail.com