Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What should happen if we perform an effect and there is no handle to catch it? #8

Closed
macabeus opened this issue Dec 23, 2019 · 42 comments
Labels

Comments

@macabeus
Copy link
Owner

macabeus commented Dec 23, 2019

What should happen in these situations?

function foo () {
  perform 'bar'
}

foo()
function foo () {
  perform 'bar'
}

try {
  foo()
} handle {
}
  • exception?
  • return a default value (maybe undefined)?

How the others languages solves this problem?

@macabeus macabeus changed the title What should happen have if we perform an effect and there is no handle to catch it? What should happen if we perform an effect and there is no handle to catch it? Dec 23, 2019
@macabeus macabeus added the spec label Dec 23, 2019
@evaporei
Copy link
Contributor

I think it should throw an error, so that every unhandled effect can be caught on catch.

@Jack-Works
Copy link

Jack-Works commented Dec 24, 2019

Consider this.
handle
setTimeout(() => eval("resume "+1))

You can't just throw.

@macabeus
Copy link
Owner Author

macabeus commented Dec 24, 2019

@Jack-Works Are you saying about when resume is on an async operation?

If it is preferable to throw an error, I think that is still possible to think about solve situations like that.
For example:

try {
  ...
} handle {
  if (effect === 'foo') {
    setTimeout(() => { resume 'Arya Stark' }, 1000);
    return // enforce to use return (maybe a lint rule?)
  }

  // if arrives on the end of "handle" block,
  // should to throw an error or at lease show a warning
}

Another approach is just return a default value (undefined?), but I don't like this approach, because it could cause a hard error to debug.

@Jack-Works
Copy link

if you want to do async resume, then you can't do handler check... they're conflict

@taylor-cedar
Copy link

taylor-cedar commented Dec 25, 2019

It seems that all effects need to be handled, otherwise you get in weird states. It also feels like this is a similar issue to the nested effects. How does it know this block doesn't handle the effect?

I think you need to register effect handlers instead, so it knows which one to use.

try {
 ...
} handle (effect === 'foo) {

} handle (effect === 'something') {

}

Here is what it desugars to. You could replicate this behavior with a library.

const handlers = [];

function handleEffect(handlerCheck, handler, fn, args=[]) {
  try {
    handlers.push([handlerCheck, handler]);
    return fn(args);
  } finally {
    handlers.pop()
  }
}

function perform(effect) {
  return new Promise((resolve) => {
      for(let i = 0; i < handlers.length; i++) {
        const [handlerCheck, handler] = handlers[i];
        if (handlerCheck(effect)) {
          handler(effect, resolve);
          return;
        }
      }
      throw new Error(`No handler for effect ${effect}`);
  });
}

Here is how to use it

async function foo () {
  await perform('bar')
}

function handler(effect, resume) {
  // Can call function sync or async
  resume('Here is the result')
}

// Handled Effect
handleEffect((effect) => effect === 'bar', handler, foo);

Nested effects

function parentFn() {
  handleEffect((effect) => effect === 'bar', (effect, resume) => resume("Result used"), nestedFn);
}

function nestedFn() {
  handleEffect((effect) => effect === 'not_foo', (effect, resume) => resume("Not used"), foo);
}
parentFn();

Missing effect

foo();

@macabeus
Copy link
Owner Author

macabeus commented Dec 25, 2019

Thank you for the ideas, but I don't like so much this syntax:

try {
 ...
} handle (effect === 'foo) {

} handle (effect === 'something') {

}

Because it'll now work well with the pattern matching proposal.

Also I can't understand very well how it'll avoid to call an effect that isn't handled. For example:

function foo () {
  const effectName = getUserInput()
  perform(effectName)
}

So we will still need to think about the situations that launch an effect that isn't handled, because even we register the effects, we'll have these situations.

@taylor-cedar
Copy link

taylor-cedar commented Dec 25, 2019 via email

@macabeus
Copy link
Owner Author

macabeus commented Dec 25, 2019

Oh, okay. Now I understand better your approach. And, yeah, it could be better than what I suggested before, to force to use return.
I like your idea.

@macabeus
Copy link
Owner Author

macabeus commented Dec 25, 2019

But just a syntax problem: how well we avoid confusion with parameter?
Because following this issue, we should to add effect as a parameter as well we already have with catch block:

try {
  ...
} catch (e) {
  ...
}

But using this same place to add a condition, we don't have anymore place to add effect as parameter, and could be confuse. Someone will think that handle (this place) { should be used to put a parameter name, instead of a condition.

@Jack-Works
Copy link

it's not weird to not handle an effect. It just likes waiting for the Promise that never resolved. It's the developer's responsibility to check if they have resolved the effect correctly and maybe the JS engine can notify the developer that it is unhandled.

If the check is enforced in the language, then we cant do async resume anymore.

@taylor-cedar
Copy link

@Jack-Works Yes, it's not unprecedented to not handle an effect, but it's not ideal. I can't think of any cases where that would have been the intended behavior. Otherwise someone would just not call the perform at all. If you see my code above, you can enforce the check and still allow async resume.

@macabeus Yes, my proposal somewhat conflicts with the effect as a param. My solution would be to make it more versatile. Here is the syntax I would suggest.

try {
  ...
} handle (effect if effect === 'some_effect') {
  resume 'result';
}

It is based on the same syntax Firefox used to have. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch#Conditional_catch-blocks

@macabeus
Copy link
Owner Author

macabeus commented Dec 26, 2019

@Jack-Works Thank you for the reply, but IMO I'm not agree with that. I prefer to be explicit, otherwise the erros could be hard to debug.
Anyway we still could have a try/handle block with default value for situations that a we really want to handle unknown effects:

try {
  ...
} handle (true) { // always will catch the effects
}

or

try {
  ...
} handle (effect if true) { // if we want to use effect variable inside of this block
  console.log(`${effect} launched`)
}

@taylor-cedar I liked this approach. It's also a little similar with array comprehensions... but both cases are non-standard and obsolete features. We need to have caution to use it.
Anyway I'll update this issue to say about this another approach, because they are conflicting.

@macabeus
Copy link
Owner Author

Also what should happen if we not call resume inside of a handle? Should will be something like a promise that never ends?

@taylorhakes
Copy link

taylorhakes commented Dec 26, 2019

@taylor-cedar I liked this approach. It's also a little similar with array comprehensions... but both cases are non-standard and obsolete features

Yep, completely agree. I think this proposal is in the discovery phase, so the exact syntax can evolve. I think the important part is having some kind of matching in the handle. It ideally would work seamlessly with the pattern matching proposal, but that hasn't been settled either.
https://github.com/tc39/proposal-pattern-matching

Also what should happen if we not call resume inside of a handle? Should will be something like a promise that never ends?

That is a problem that I would like to solve as well (make it not possible). My feeling would be to force resume to be the last statement of the handle or make it a function instead. For async, just use the normal async/await syntax, instead of resume being called anywhere for async.

try {

} handle (effect if effect === 'something') {
  await doSomethingAsync();
  resume 'Result';
}

Little weird, but the resume ends up being a return, so maybe handle is function like? Just throwing out ideas.

try {

} handle (effect if effect === 'something') => {
  await doSomethingAsync();
  return 'Result';
}

@macabeus
Copy link
Owner Author

macabeus commented Dec 26, 2019

@taylor-cedar There is an issue about use resume on others places. If we'll allows it, it'll not work, because we could have something like that:

function foo () {
  resume 'foo'
}

try {
  ....
} handle (effect if true) {
  foo() // I'm calling resume, but the compiler will can't see it
}

Anyway, both cases (enforcing "resume" or using a function) will break if we use a callback:

try {
  ....
} handle (effect if true) => {
  setTimeout(
    () => 'foo',
    1000
  ) // we want to return 'foo', but it'll return undefined
}

@taylor-cedar
Copy link

taylor-cedar commented Dec 26, 2019

@macabeus I am suggesting not allowing a callback or called function. Similar to how yield or await are not allowed in a callback. I don't believe it would ever be feasible to allow it in callbacks, since it's only allowed in specific places and changes the behavior of the function where it exists. My suggestion is someone wants to do setTimeout, they have to do the same thing as with await.

function wait(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
}

try {
  ....
} handle (effect if true) {
  await wait(1000);
  resume 'result';
}

If it's a function like, you don't have to worry about resume in other functions, since resume doesn't exist. You can use return like normal functions

function foo () {
  return 'foo'
}

try {
  ....
} handle (effect if true) => {
  return foo() // No longer using resume. All functions understand `return`
}

@lukeed
Copy link

lukeed commented Dec 27, 2019

Having a clause condition embedded within the parameter declaration would/should be a separate proposal. That's an entirely different language feature that a) does not currently exist anywhere b) could be applied in multiple places.

This type of guards/pattern-matching is available in other languages (eg, Elixir) as a means to avoid if, switch, and match statements. That would also be the implication in JavaScript, if applied.

Trying to add this here (and only here) does not fit IMO.


FWIW, I think unhandled effects should operate like unhandled Promise exceptions: it must throw a runtime error.

It's already been suggested here, but with #16 in place, default effect handling is straightforward:

// sync 
function run() {
  try {
    foo();
  } handle (effect) {
    switch (effect) {
      case 'foobar':
        throw new Error('foobar');
        break;
      // case '...';
      default: 
        cleanup();
  } catch (error) {
    // ..
  } 
}

// async 
async function run() {
  try {
    await foo();
  } handle (effect) {
    switch (effect) {
      case 'foobar':
        throw new Error('foobar');
        break;
      // case '...';
      default: 
        await cleanup();
  } catch (error) {
    // ..
  } 
}

As shown, handling async side effects follows all other JS patterns.

Also shown is the idea that errors thrown from within the handle block should be handled within the paired catch block. I think this feels right/natural, but not 100% sure. The alternative would be to run the effects within an additional try/catch block.

@Jack-Works
Copy link

There are two more solutions to this.

The first one: resume can be async, but it must appear in the sub syntax tree of the handle clause.
For example

try...
handle (effect) {
    resume await result
}

The second one: we can learn from Service Worker.

handle(effect) {
    effect.waitUntil(result) // the waitUntil or .resume() must be call synchronized 
}

@taylor-cedar
Copy link

taylor-cedar commented Dec 27, 2019

@lukeed I agree that if conditionals in the block can be a separate proposal if we agree that you can't have resume inside a callback or another function. i.e.

try {

} handle (effect) {
  setTimeout(() => {
     // Not allowed. resume must be in handle block directly
    resume "Something";
  }, 10);
}

also not allowed

function someOtherFn() {
  // Not allowed. resume must be in handle block directly
  resume "Something"
}

try {

} handle (effect) {
  someOtherFn();
}

If we also only allow async through async/await or iterators, we can throw an error if resume is not called at the end of the handle and there are no handles higher in the scope. Similar to uncaught exceptions, except entering a handleblock does not consider it handled.

try {
} handle (effect) {
  // Doing some async
  const result = await somePromiseFn();
  resume result;
}

As for catch after a handle. If we force catch (if specified) to be after handle, just like how catch has to be before finally today, it make sense.

try {
} handle (effect) {
} catch (error) {

} finally {

}

@lukeed
Copy link

lukeed commented Dec 27, 2019

If there's to be a handle counterpart, then yes, it should only be within handle.

I don't like adding resume personally. It's unnecessary. We already have async/await for asynchronous control flow.

@taylor-cedar
Copy link

I don't like adding resume personally. It's unnecessary. We already have async/await for asynchronous control flow.

This whole proposal is about adding a new control flow. How do you go back to the perform statement?

@lukeed
Copy link

lukeed commented Dec 28, 2019

The effect's handler finishes execution, which reinforces requiring one.

@taylor-cedar
Copy link

taylor-cedar commented Dec 28, 2019 via email

@lukeed
Copy link

lukeed commented Dec 28, 2019

Can you elaborate "nesting handles"? Is that just when an effect's handler had its own perform?

To me, perform is just a way to say "go find & run the action for identifier". It pairs directly to a function that's not necessarily imported in the same scope/file as the thing that initiated the effect. So, following that logic, any handler(s) get executed/chained/nested as if you were dealing directly with functions.

function getName(user) {
 return user.name || perform 'ask_name';
}

async function displayName(user) {
 const name = await getName(user);
 return name.toUpperCase();
}

const arya = { name: null };

try {
  const str = await displayName(arya);
  console.log(str);
} handle (eff) {
  if (eff === 'ask_name') {
    return 'Arya Stark'; // sync
    // or
    return sleep(1e3).then(() => 'Arya Stark'); // async
  }
}

Runs the same as:

function getName(user) {
 // return user.name || perform 'ask_name';
 return user.name || 'Arya Stark'; // sync
 // or
 return user.name || sleep(1e3).then(() => 'Arya Stark'); // async
}

async function displayName(user) {
 const name = await getName(user);
 return name.toUpperCase();
}

const arya = { name: null };

try {
  const str = await displayName(arya);
  console.log(str);
} handle (eff) {
  // not needed
}

@taylor-cedar
Copy link

taylor-cedar commented Dec 30, 2019

Ok, if I am understanding correctly, you are saying perform/resume is not even necessary because you could just call a function directly using async/await? If so, that is definitely off topic for this issue. It should be created as a different issue.

To answer your question quickly without derailing, it's changing who has control over the side effects.

Below the caller is deciding what function to call. If you are controlling/writing this code and wanted to change its behavior, you would just change the function that's called, simple.

return sleep(1e3).then(() => 'Arya Stark');

Let's say this was a library that you didn't control. The library wanted to allow you to implement your own handler (how to get the user name). Generally the pattern that is currently used today is passing in some kind of config that specifies the function you want to use. This proposal is implementing a different mechanism instead.

Inside the library:

return user.name or perform {type: 'getUserName' id: user.id};

Outside the library

 try {
  // Some library function. I.E.
  renderUserComponent({id: 2})
} handle (effect) {
  const result = await getUserAsync({id: effect.id});
  resume user.name
}

It also makes testing really nice, you can change your implementation to return a fake value without having to monkeypatch/override your async function.

To discuss more, let's create another ticket.

@macabeus
Copy link
Owner Author

macabeus commented Dec 31, 2019

I was travelling, so I'm just commenting now after read the all answered. Very thank you to help in this discussion!

There are many nice suggestions and ideas here, and each idea has different trade offs. I'm doing a recap here, because I think that we are a little mess.

Objective: we want to discover the best act when perform an effect and there is no handle to catch it AND avoid problems of resume inside of handle block

Do nothing (first mention here)

We always need to think about the simplest solution: do nothing. We could simply return a default value (undefined?) if perform an effect and no one catch it

function foo () {
  const name = perform 'ask_name'
  console.log(name)
}

foo() // prints undefined

try {
  foo() // prints 'Arya Stark'
} handle (effect) {
  resume 'Arya Stark'
}

Good:

  • simplest solution
  • easy to understand

Bad:

  • could be very hard to debug
  • it doesn't work very well with nested effect handles (how we'll know if should to check the past effect handle block?)
  • it doesn't work with async/callback effects handle (how we'll know if should return the default value or wait something that will return a value?)

Add clause condition embedded within the parameter declaration (first mention here)

try {
  ...
} handle (effect if effect === 'ask_name') {
  ...
}

So if we perform an effect and no one condition be true, should throw an exception.

Good:

Bad:

Doesn't allow to have resume inside a callback or at another function (first mention here)

try {
  ...
} handle (effect) {
  setTimeout(() => {
     // Not allowed. resume must be in handle block directly
    resume "Something";
  }, 10);
}
function someOtherFn() {
  // Not allowed. resume must be in handle block directly
  resume "Something"
}

try {
  ...
} handle (effect) {
  someOtherFn();
}
try {
  ...
} handle (effect) {
  // Allowed!
  const result = await somePromiseFn();
  resume result;
}

Good:

  • easy to ensure that we are calling resume on a handle block

Bad:

  • it won't work with nested effect handle (how we'll know if should to check the past effect handle block?)
  • by design it isn't so much flexible solution (enforce to use async/await, for example)

Anyway... I don't think that the second and third ideas are mutually exclusive. But IMO I don't like so much the restrictions of the third idea. I prefer to have a more flexible code and could have "promises that never ends".

IMO we could go with the second approach, mainly because it work well with nested effect handlers AND it could give a safer code because it raises errors if launch an effect that doesn't have a handle to catch.

What do you think?

@taylor-cedar
Copy link

taylor-cedar commented Dec 31, 2019

Thanks for the summary. I think you summed it up nicely. Based on messing around with the syntaxes and limitations you mention above for the last few days, I am strongly in favor of number 3. Here is why:

  • I am very confident allowing resume in any function (callbacks, different functions) has no chance of being approved as a language addition. It's too dynamic/magical and changes the behavior of every function in the stack, not just the one that contains resume. This proposal is already such a difficult thing to propose, I definitely wouldn't want to make it worse. Async can already be done with async/await
  • Add clause condition embedded (which I proposed) I realize now can be added later, just like it can be added to catch in the future. I don't want to add complexity to the proposal by adding a new matching syntax.

How will nested handle work and how will we know if an effect is not handled? Just like catch does today.

// Exceptions
try {

} catch (error) {
  if (error === 'something) {
    handleError(error);
  } else {
    // Don't recognize it, rethrow the error so nested catch can get it
    throw error;
  }
}

// Handles
try {

} handle (effect) {
  if (effect.type === 'something') {
    const result = async getSomething(effect.id);
    resume result;
  } else {
    // Don't recognize it, let it go to the nested handle
    perform effect;
  }
}

There is a nice symmetry with exceptions. The slight difference is that I would throw an error if resume or perform is not called in handle. That would allow developers to find a mistake.

@lukeed
Copy link

lukeed commented Dec 31, 2019

Third option, without question.

Agreed re: resume-vs-return discussion – that's a separate discussion, sorry

@macabeus
Copy link
Owner Author

@taylor-cedar I liked the symmetry with exceptions, but with this approach would be hard to compose nested effects, because I always need to explicitly "re-perform" at the end of handle block. It could be bad especially if we are using a third-package and we want to add more effects.

I know that my example code could be not so much common, but is just to illustrate the situation:

// code on a third package
function thirdPackageFunction (callback) {
  try {
    callback()
  } handle (effect) {
    if (effect === 'ask_name') {
      resume 'Arya Stark'
    }

    // since I'm in my package, makes sense to throw an error here
    // instead of "re-performing"
    throw 'Error! Unknown effect'
  }
}
function getNameAndAge () {
  const name = perform 'ask_name'
  const age = perform 'ask_age'
  return { name, age }
}

try {
  thirdPackageFunction(getNameAndAge)
} handle (effect) {
    // FAIL! It will be never called,
    // because "thirdPackageFunction" isn't "re-performing"
    if (effect === 'ask_age') {
      resume 25
    }
}

MAYBE another approach is implicitly always "re-performing" at the end of handle block is no perform was called, but I don't know how much good is a implicitly behaviour like that...

@lukeed

Agreed re: resume-vs-return discussion – that's a separate discussion, sorry

Yeah, we could discuss about that here: #14
I'm agree that we could avoid to add many new keywords/reserved word.

@taylor-cedar
Copy link

taylor-cedar commented Dec 31, 2019

@macabeus I agree that you will need to reperform in some cases (maybe a lot) at the end of the block. That is the case for catch as well. If a library adds a new exception, you probably want to rethrow it.

try {

} catch (error) {
  if (error.message === 'somethingKnown') {
    // .. do something
  } else {
    // New exception I don't understand rethrow
   throw error;
  }
}

The way to get around this issue (same for exceptions/handles) is to make the block conditional.

try {

} catch (error if error.message === 'somethingKnown') {
  // .. do something
}

The same fix for both handle and catch work, but that should be a different proposal as I discussed above.

@lukeed
Copy link

lukeed commented Dec 31, 2019

@taylor-cedar This is a new JS feature & is (should be) completely unrelated to the perform/handle/resume group.

If you are saying that block-conditionals are necessary in order to this proposal to work, then:

  1. I disagree with you
  2. that means that this proposal is not viable

@macabeus
Copy link
Owner Author

We can follow without block-conditionals, but we'll need to think a good approach to compose nested effects.

Thinking again now, maybe would be good to always "re-performing" at the end of a handle block if none resume was called, and if there is no handle anymore, throw an exception (on the same level of the first perform).

@lukeed
Copy link

lukeed commented Dec 31, 2019

IMO it should operate the same as try/catch – every effect/error bubbles upwards. This means that if you run a perform outside of a try/handle, it will bubble up to the next scope that does have a handle-block.

Should the effect reach the global scope (meaning it was unhandled), then a runtime error is thrown.
This is the same behavior as any Errors or uncaught Promise exceptions.

@taylor-cedar
Copy link

taylor-cedar commented Dec 31, 2019

Sorry guys my previous message above, was meant for @macabeus, not @lukeed. It was a typo.

@lukeed I wrote the following.

The same fix for both handle and catch work, but that should be a different proposal as I discussed above.

I am against any matching syntax. I explicitly said that should be a different proposal (not this proposal). I was just showing how it will work in the future after a matching syntax is added, again unrelated, would complement this proposal in the future.

@macabeus I agree with @lukeed that bubbling/composing effects should be done by calling perform again, just like you need to call throw again in catch for the exception to bubble.

@macabeus
Copy link
Owner Author

macabeus commented Jan 1, 2020

@macabeus I agree with @lukeed that bubbling/composing effects should be done by calling perform again, just like you need to call throw again in catch for the exception to bubble.

Okay. It's fine for me. But just one question to ensure if I understand the idea: there is no implicit re-perform?
For example:

function sayNameAndAge () {
  const name = perform 'ask_name'
  const age = perform 'ask_age'
  console.log(name, age)
}

function foo () {
  try {
    sayNameAndAge()
  } handle (effect) {
    if (effect === 'ask_age') {
      resume 25
    }

    // we explicitly need to re-perform where,
    // otherwise "ask_name" will throw an exception
    perform effect
  }
}

try {
  foo()
} handle (effect) {
  if (effect === 'ask_name') {
    resume 'Arya Stark'
  }
}

I'll start tomorrow to update the babel plugin with these updates (and write an issue with the changes), so I would like to understand better it.

Thank you for the nice ideas =]

@lukeed
Copy link

lukeed commented Jan 1, 2020

Correct, to behave like try/catch with errors, you'd need to manually re-throw (or re-perform) to continue the bubbling.

The only "drawback" here is that if you forget to manually re-perform unhandled effects, then the effect would silently disappear – and JS wouldn't be able to throw a runtime error. However, this is equally true for errors and unhandled Promises, so we must follow the pattern.

Here's example w/ try-catch and errors. It should be identical if you substitute everything for effects instead of errors:

const foo = () => { throw new Error('foo') }
const bar = () => { throw new Error('bar') }

try {
  foo(); 
  bar();
} catch (err) {
  if (err.message === 'foo') {
    console.log('caught: foo')
  }
}
// => caught: foo

@macabeus
Copy link
Owner Author

macabeus commented Jan 1, 2020

The only "drawback" here is that if you forget to manually re-perform unhandled effects, then the effect would silently disappear – and JS wouldn't be able to throw a runtime error.

Since perform is an expression, we always need to return a value.
So in this case we should return undefined? For example:

function sayName () {
  const name = perform 'ask_name'
  console.log(name) // should print undefined
}

try {
  sayName()
} handle (effect) {
  // nothing here
}

@lukeed
Copy link

lukeed commented Jan 1, 2020

Yes IMO.

Unless there's no handle block at all, then an Error is thrown:

function sayName () {
  const name = perform 'ask_name'
  // throw new Error('Unhandled effect')
  console.log(name) // does not reach here
}

sayName()

//=> Error: Unhandled effect: 'ask_name'

@taylor-cedar
Copy link

taylor-cedar commented Jan 1, 2020

@lukeed @macabeus I agree. I think it makes sense to return undefined, so it matches the behavior of a function without return. It should throw an error when no handle block.

This was referenced Jan 3, 2020
@macabeus
Copy link
Owner Author

macabeus commented Jan 3, 2020

I'm closing this issue because there is a PR implementing the ideas discussed here.
Any others discussion could be done in PR or in a new issue

@macabeus macabeus closed this as completed Jan 3, 2020
@macabeus
Copy link
Owner Author

macabeus commented Jan 3, 2020

@lukeed @taylor-cedar @Jack-Works @otaviopace I know that my code in Babel plugin is very shit (because I'm learning how to write one), but you could take a look on the PR and see the others discussions on TODO section, please? Thank you 😋

@macabeus macabeus mentioned this issue Apr 4, 2020
@reverofevil
Copy link

reverofevil commented Jun 15, 2021

Sorry, but third option makes these effects whatever but algebraic effects. The whole point is that (unhindered) algebraic effects cover all possible cases of delimited control. There must be an ability to call resume zero to infinity times, anywhere in handle clause. resume is a function, a continuation. Limiting how many times and from where a function is called makes no sense.

We already have exceptions, generators, promises and async/await. They are all partial implementations of delimited control, and nobody needs yet another partial implementation in JS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

7 participants