One-shot Delimited Continuations with Effect Handlers
Ben Newman
benjamin at cs.stanford.edu
Wed Mar 16 00:56:33 UTC 2016
Ok, I think I understand how your needs differ from the async/relaxed-await
idea. You need a way to
- extract values from deeply nested yields, which is not allowed by
await expressions,
- resume execution whenever you choose, which is a decision await
expressions make for you, i.e. they resume whenever the Promise is
resolved/rejected, and
- execute everything synchronously (if desired).
Perhaps if there was a way to wrap any arbitrary expression with a
generator that captured any yielded values and allowed resumption by
calling .next(), then you could accomplish this without inventing new
try-catch syntax?
On Tue, Mar 15, 2016 at 8:27 PM Sebastian Markbåge <sebastian at calyptus.eu>
wrote:
> async functions only address the async use case and they're all scheduled
> on a simple micro-task queue. We need fine grained control over scheduling.
> Perhaps Zones can help a bit with that but that's just one of severals
> concepts that need this.
>
> It doesn't solve other more general generator use cases. You could
> potentially expand it to generators as well.
>
> However, then you'd also need to solve the nested handlers case
> efficiently. That's my use case. What if you have a layout handler, in an
> iteration handler in an async scheduler handler?
>
> The async functions could be implemented in terms of this though since
> they're just a more specific and locked down version.
>
>
>
> On Tue, Mar 15, 2016 at 5:16 PM, Ben Newman <ben at benjamn.com> wrote:
>
>> What if we simply allowed await expressions anywhere in the call stack of
>> an async function, rather than only in the bodies of async functions? That
>> would give us all the power of "yielding deep in a fiber" with a much more
>> familiar syntax, and an easy way to capture effects, since an async
>> function returns a Promise that allows for asynchronous error handling. No
>> new catch effect … syntax needed.
>>
>> I've talked about this before
>> <http://benjamn.github.io/goto2015-talk/#/17>, and it's my understanding
>> that TC39 needs a lot of convincing that coroutines are a good idea. We
>> haven't really discussed the topic in the context of async functions, which
>> are soon to be an official part of the language, so perhaps the time for
>> discussing coroutines/continuations/etc. is drawing near!
>>
>> On Tue, Mar 15, 2016 at 7:44 PM Sebastian Markbåge <sebastian at calyptus.eu>
>> wrote:
>>
>>> Has anyone previously proposed a Algebraic Effects (e.g. Eff like
>>> handlers of continuations) for ECMAScript? #lazyweb I could only find
>>> variations that aren't quite this.
>>>
>>> I'm specifically looking at the OCaml implementation for inspiration:
>>>
>>> http://kcsrk.info/ocaml/multicore/2015/05/20/effects-multicore/
>>>
>>> http://kcsrk.info/slides/multicore_fb16.pdf
>>>
>>> I'm not proposing the multicore aspect of this but just the delimited
>>> continuations.
>>>
>>> Basically, the idea is that you can capture effects by wrapping a call
>>> in a "try". That spawns a fiber. Then any function can "perform" an effect
>>> as a new language feature. That works effectively like "throw" expect it
>>> also captures a reified continuation, in a tuple. The continuation can be
>>> invoked to continue where you left off.
>>>
>>> Imaginary syntax:
>>>
>>> function otherFunction() {
>>> console.log(1);
>>> let a = perform { x: 1, y: 2 };
>>> console.log(a);
>>> return a;
>>> }
>>>
>>> do try {
>>> let b = otherFunction();
>>> b + 1;
>>> } catch effect -> [{ x, y }, continuation] {
>>> console.log(2);
>>> let c = continuation(x + y);
>>> console.log(c);
>>> }
>>>
>>> Prints:
>>> 1
>>> 2
>>> 3
>>> 4
>>>
>>> (`perform` is a contextual keyword to "throw" and `catch effect` is a
>>> keyword for catching it.)
>>>
>>> We've experimented with changing React's implementation to use these
>>> internally to handle concurrency and being able to solve complex algorithms
>>> that require a lot of back and forth such as layout calculation. It seems
>>> to make these implementations much easier while remaining efficient.
>>>
>>> It also allows for seamless async I/O handling by yielding deep in a
>>> fiber.
>>>
>>> Effectively this is just solving the same thing as generator and
>>> async/await.
>>>
>>> However, the benefit is that you don't have a split between "async"
>>> functions, generator functions and synchronous functions. You still have an
>>> explicit entry point through the place where you catch effects.
>>>
>>> With generators and async functions, anytime you want to change any deep
>>> effects you have to unwind all potential callers. Any intermediate library
>>> have to be turned into async functions. The refactoring is painful and
>>> leaves you with a lot of syntax overhead.
>>>
>>> If you want to nest different effects such as layout, iterations and
>>> async functions that complexity explodes because now every intermediate
>>> function has to be able to handle all those concepts.
>>>
>>> The performance characteristics demonstrated by KC Sivaramakrishnan are
>>> also much more promising than JS VMs has been able to do with async/await
>>> and generators so far. It's plausible that VMs can optimize this in similar
>>> way, in time. I suspect that the leakiness of the microtask queue might
>>> cause problems though.
>>>
>>> I converted the OCaml example scheduler to this ECMAScript compatible
>>> syntax:
>>>
>>> // RoundRobinScheduler.js
>>>
>>> class Fork {
>>> constructor(fn) {
>>> this.fn = fn;
>>> }
>>> }
>>> function fork(f) {
>>> perform new Fork(f)
>>> }
>>>
>>> class Yield { }
>>> function yieldToScheduler() {
>>> perform new Yield();
>>> }
>>>
>>> export function run(main) {
>>> const run_q = [];
>>> function enqueue(k) {
>>> run_q.push(k);
>>> }
>>> function dequeue() {
>>> if (run_q.length) {
>>> run_q.shift()();
>>> }
>>> }
>>> function spawn(f) {
>>> try {
>>> f();
>>> dequeue();
>>> } catch (e) {
>>> console.log(e.toString());
>>> } catch effect Yield -> [_, k] {
>>> enqueue(k);
>>> dequeue();
>>> } catch effect Fork -> [fork, k] {
>>> enqueue(k);
>>> spawn(fork.fn);
>>> }
>>> }
>>> spawn(main);
>>> }
>>>
>>> // Example.js
>>>
>>> import * as Sched from "RoundRobinScheduler";
>>>
>>> function f(id, depth) {
>>> console.log("Starting number %i", id);
>>> if (depth > 0) {
>>> console.log("Forking number %i", id * 2 + 1);
>>> Sched.fork(() => f(id * 2 + 1, depth - 1));
>>> console.log("Forking number %i", id * 2 + 2);
>>> Sched.fork(() => f(id * 2 + 2, depth - 1));
>>> } else {
>>> console.log("Yielding in number %i", id);
>>> Sched.yield();
>>> console.log("Resumed number %i", id);
>>> }
>>> console.log("Finishing number %i", id);
>>> }
>>>
>>> Sched.run(() => f(0, 2));
>>>
>>> _______________________________________________
>>> es-discuss mailing list
>>> es-discuss at mozilla.org
>>> https://mail.mozilla.org/listinfo/es-discuss
>>>
>>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20160316/cdb6c9b4/attachment.html>
More information about the es-discuss
mailing list