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:

Re: One-shot Delimited Continuations with Effect Handlers

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, 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!

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:

Re: One-shot Delimited Continuations with Effect Handlers

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.

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, 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!

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:

Re: One-shot Delimited Continuations with Effect Handlers

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?

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.

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, 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!

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:

Re: One-shot Delimited Continuations with Effect Handlers

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?

Yea, except you need to be able to nest them inside each other as well.

If a generator captured any yielded values, then it would be yielded at the inner most caller.

You could handle this the way JavaScript does exception handling by "rethrowing" errors it didn't handle. I.e. if you see a yield that you don't recognize you would re-yield it.

In my use case I can have many nested handlers and I want to handle a particular type at the top of the stack. If you have to conditionally "reyield" all the way up there, you miss out on potential important optimizations.

This is related to "enums" and "pattern matching" too. The current pattern matching proposal also doesn't have any optimizations either.

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?

Yea, except you need to be able to nest them inside each other as well.

If a generator captured any yielded values, then it would be yielded at the inner most caller.

You could handle this the way JavaScript does exception handling by "rethrowing" errors it didn't handle. I.e. if you see a yield that you don't recognize you would re-yield it.

In my use case I can have many nested handlers and I want to handle a particular type at the top of the stack. If you have to conditionally "reyield" all the way up there, you miss out on potential important optimizations.

This is related to "enums" and "pattern matching" too. The current pattern matching proposal also doesn't have any optimizations either.

RE: One-shot Delimited Continuations with Effect Handlers

> 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.

Isn't the problem we actually need to solve here the fact we're not able to control scheduling or context in async functions?

Other languages with async functions like Python and C# provide the means to control the scheduling of async functions.

This is also indeed deeply related to zones since there is no inherent reason the same doesn't apply to async things that are not promises (like observables, async iterators and event emitters).

Re: One-shot Delimited Continuations with Effect Handlers

The effect addition to try-catch seems like some sort of hacky
workaround, that would get the job done, but then would make try-catch
be used for purposes other than catching errors, which defeats it's
original purpose. I think it's important to keep that error-based
meaning and not mix it with anything else. "Try this, catch errors"
and that's all.

> 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?

Although it is more work for the person writing code, I believe having
to explicitly use keywords (await or yield) in function bodies makes
it very clear what is happening, and ultimately leads to better code
with less potential for human error in the long run. I would vote
against allowing a relaxed await anywhere in the call stack. One of
the pains I had with Java was realizing after debugging for a while
that something I was doing was async (I was new to Java).

The beauty of JavaScript from the very beginning (the reason I love
JavaScript) is that dealing with asynchronous behavior is something a
JavaScript developer is forced to do from the get go. Introducing
invisible asynchronous behavior would deviate from that (and be more
like Java). It might be suitable at first, for new programmers, but as
soon as they have problems, the source of those problems could be
invisible unless they go read the docs on every API (and if lucky,
that API mentions that a function is async). Requiring `await` will
force everyone to learn how to deal with async behavior from the get
go, just like we all had to learn how to deal with callback hell from
the get go, which was a good thing (despite the syntax "hell").

>> 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.
>
> Isn't the problem we actually need to solve here the fact we're not able to
> control scheduling or context in async functions?
> Other languages with async functions like Python and C# provide the means to
> control the scheduling of async functions.
>
> This is also indeed deeply related to zones since there is no inherent
> reason the same doesn't apply to async things that are not promises (like
> observables, async iterators and event emitters).
>
> _______________________________________________
> es-discuss mailing list
> [hidden email]> https://mail.mozilla.org/listinfo/es-discuss>

Re: One-shot Delimited Continuations with Effect Handlers

Although it is more work for the person writing code, I believe having
to explicitly use keywords (await or yield) in function bodies makes
it very clear what is happening, and ultimately leads to better code
with less potential for human error in the long run.
...
The beauty of JavaScript from the very beginning (the reason I love
JavaScript) is that dealing with asynchronous behavior is something a
JavaScript developer is forced to do from the get go.

I start from the premise that this explicitness is already a huge and unmanageable problem through observation. Note my observation that nesting various types of effects (async isn't the only one) makes this totally unmanageable. If it wasn't, the status quo would be fine, but it isn't.

Introducing invisible asynchronous behavior would deviate from that (and be more like Java).

The beauty of algebraic effects is that these side-effects can't just randomly leak. If you're concerned about any particular code path having these side-effects you can catch all the effects in that code-path. This is very much unlike Java. That way you opt-in to that guarantee when you need it, instead of forcing every little thing along the way make that decision.

> we're not able to control scheduling or context in async functions?

I think we can:

Your example demonstrates that explicitness can solve it but that is exactly the problem that this proposal is trying to address. If you don't agree with the premise that this is unmanageable there isn't much more to it.