An effect handler implementation using JavaScript/TypeScript Generators that mimics OCaml's deep effect handlers.
Here is explained how to define and use effects using Defer
as an example. The full code is available in examples/defer.ts.
-
Define an effect by extending the
Effect
class.Effect
takes a type parameter that represents the return type of the effectful computation,void
in this case.class Defer extends Effect<void> { // Defer carries a nullary function that will be invoked later. thunk: () => void; constructor(thunk: () => void) { super(); this.thunk = thunk; } }
-
Define a helper function that simply performs the
Defer
effect defined above.const defer = (thunk: () => void) => perform(new Defer(thunk));
-
Define a handler for the
Defer
effect usingmatchWith
.matchWith
is liketry
-catch
in JavaScript, but for effects.function runDefer<T>(comp: Effectful<T>) { // Save thunks to be executed later. const thunks: (() => void)[] = [];
The
matchWith
function takes two arguments. The first argument is the effectful computation to be handled.return matchWith(comp, {
The second argument is an object that contains three handlers:
retc
,exnc
, andeffc
.retc
is the value handler, which is called when the computation returns a value. In thisDefer
example, execute the saved thunks then return the value as is.retc(x) { thunks.forEach((thunk) => thunk()); return x; },
exnc
is the exception handler, which is called when the computation throws an exception. Here, likeretc
, execute the saved thunks then rethrow the exception.exnc(exn) { thunks.forEach((thunk) => thunk()); throw exn; },
effc
is the effect handler, which gets called when the computation performs effects.on
, the argument ofeffc
, is for registering handlers for each effect. The first argument ofon
is the constructor of an effect (sayE
) and the second argument is a handler function that takes an effect value of typeE
and its continuation. In this example, save the thunk carried before resuming the computation. Note that you do not need to handle all of effects in one handler. Effects not handled are passed to the surrounding handler.effc(on) { on(Defer, (eff: Defer, cont: Continuation<void, T>) => { thunks.unshift(eff.thunk); return cont.continue(); }); }, }); }
Here is the full code for the
runDefer
function.function runDefer<T>(comp: Effectful<T>) { // Save thunks to be executed later. const thunks: (() => void)[] = []; return matchWith(comp, { retc(x) { thunks.forEach((thunk) => thunk()); return x; }, exnc(exn) { thunks.forEach((thunk) => thunk()); throw exn; }, effc(on) { // The type annotaion for the handler function is optional as it can be inferred. on(Defer, (eff, cont) => { thunks.unshift(eff.thunk); return cont.continue(); }); }, }); }
-
Now you are ready to use the
Defer
effect. Define an effectful computation using generator functions. Use theyield*
keword to perform effects like theawait
keyword inasync
functions.function* main(): Effectful<void> { console.log("counting"); for (let i = 0; i < 10; i++) { yield* defer(() => console.log(i)); } console.log("done"); }
-
Finally, run the effectful computation using the
runEffectful
function after wrapping it with the handler.runEffectful(runDefer(main())); /* Output: counting done 9 8 7 6 5 4 3 2 1 0 */
For more examples, including the state
effects, see the examples directory.
An effectful computation can be represented as a generator that yields effects.
🚧 TODO