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.