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

Way to synchronize on async operations inside a halt() #35

Closed
cowboyd opened this issue Nov 18, 2019 · 2 comments
Closed

Way to synchronize on async operations inside a halt() #35

cowboyd opened this issue Nov 18, 2019 · 2 comments
Labels
enhancement New feature or request
Milestone

Comments

@cowboyd
Copy link
Member

cowboyd commented Nov 18, 2019

Sometimes you want to do asynchronous cleanup. For example, in @bigtestjs/server we would really like the parcel server halt code. Unfortunately, you cannot yield inside a generator that has already returned or thrown. We'd like the halt() to not be considered complete until some async code has run. e.g.

let bundler = new Bundler();
try {
  yield;
} finally {
  yield bundler.stop() <-- cannot be done.
}
@cowboyd cowboyd added enhancement New feature or request and removed triage labels Nov 20, 2019
cowboyd added a commit that referenced this issue Dec 15, 2019
Originally, this change came out of problems with sequencing a
fork. Specifically, if there was an error inside of fork, and so it
tried to throw() inside of its parent, then the error was hidden by
the fact that [its parent generator was already running]. This was
symptomatic of a deeper problem of the generator syntax being too
tightly coupled to the underlying concept of the execution of a
sequence of steps, and also of the concept of synchronous and
asynchronous execution being too coupled.

As a purely practical consideration, we wondered "what if the parent
generator was yielded at the time we tried to create the fork?" That
way, the fork would be either be at its first yield point, or have
failed when we tried to resume the parent, but this turned out to be a
profoundly positive re-orientation of the effection architecture to
separate out the concepts of context and executions from the generator
synax entirely which enables all sorts of nice things.

Fundamentally, this change makes effection a function-oriented effects
library rather than a generator-oriented effects library. That is, you
don't _need_ generators to declare a tree of processes, it's just
nicer to do it that way. For example:

```js

enter(join(fork(({ resume, ensure })=> resume(5))))
```

The nice effect that this has is that the generator syntax is
implemented as just a plugin to the basic runtime, which means that in
addition to solving the problem at hand, it also [allows you to fork a
control function directly][2], something that had always felt like it
should work, but annoyingly didn't.

This feels like the right direction to take it, because it is the
solution to several seemingly unrelated problems, and it even feels as
though it could provide a pathway to an [asynchronous halt
operation][3] by making not just `fork` but also `halt` an operation.

Finally, because structural concurrency is implemented ultimately as
operations that do nothing more than manipulate the execution context,
other structured effects (state, messaging, etc...) can be implemented
in user space. Using this library, I was able to implement messaging
operations based on the [send-and-receive][4] branch completely in
user-space, and in which both `send()` and `receive()` were
operations.

There are still some kinks to be worked out. For example, it's
awkward to jump into generator syntax from function syntaxt (nothing
blocking though, and certainly, nothing worse than what came before
before).

From the perspective of the JavaScript runtime, execution is still
completely synchronous internally, and the code is much, much simpler
and (in my opinion) easier to follow.

[1]: #26
[2]: #33
[3]: #35
[4]: #49
cowboyd added a commit that referenced this issue Dec 15, 2019
Originally, this change came out of problems with sequencing a
fork. Specifically, if there was an error inside of fork, and so it
tried to throw() inside of its parent, then the error was hidden by
the fact that [its parent generator was already running]. This was
symptomatic of a deeper problem of the generator syntax being too
tightly coupled to the underlying concept of the execution of a
sequence of steps, and also of the concept of synchronous and
asynchronous execution being too coupled.

As a purely practical consideration, we wondered "what if the parent
generator was yielded at the time we tried to create the fork?" That
way, the fork would be either be at its first yield point, or have
failed when we tried to resume the parent, but this turned out to be a
profoundly positive re-orientation of the effection architecture to
separate out the concepts of context and executions from the generator
synax entirely which enables all sorts of nice things.

Fundamentally, this change makes effection a function-oriented effects
library rather than a generator-oriented effects library. That is, you
don't _need_ generators to declare a tree of processes, it's just
nicer to do it that way. For example:

```js

enter(join(fork(({ resume, ensure })=> resume(5))))
```

The nice effect that this has is that the generator syntax is
implemented as just a plugin to the basic runtime, which means that in
addition to solving the problem at hand, it also [allows you to fork a
control function directly][2], something that had always felt like it
should work, but annoyingly didn't.

This feels like the right direction to take it, because it is the
solution to several seemingly unrelated problems, and it even feels as
though it could provide a pathway to an [asynchronous halt
operation][3] by making not just `fork` but also `halt` an operation.

Finally, because structural concurrency is implemented ultimately as
operations that do nothing more than manipulate the execution context,
other structured effects (state, messaging, etc...) can be implemented
in user space. Using this library, I was able to implement messaging
operations based on the [send-and-receive][4] branch completely in
user-space, and in which both `send()` and `receive()` were
operations.

There are still some kinks to be worked out. For example, it's
awkward to jump into generator syntax from function syntaxt (nothing
blocking though, and certainly, nothing worse than what came before
before).

From the perspective of the JavaScript runtime, execution is still
completely synchronous internally, and the code is much, much simpler
and (in my opinion) easier to follow.

[1]: #26
[2]: #33
[3]: #35
[4]: #49
@jnicklas
Copy link
Collaborator

Somewhat related to this: it should also be possible to yield to a halt. Ideally, I think, halt should return a promise, since we may also want to do this from outside of the effection world, e.g.:

afterEach(async function() {
  await someExecution.halt()
});

cowboyd added a commit that referenced this issue Jan 15, 2020
Motivation
----------

Originally, this change came out of problems with sequencing a
fork. Specifically, if there was an error inside of fork, and so it
tried to throw() inside of its parent, then the error was hidden by
the fact that [its parent generator was already running]. This was
symptomatic of a deeper problem of the generator syntax being too
tightly coupled to the underlying concept of the execution of a
sequence of steps, and also of the concept of synchronous and
asynchronous execution being too coupled.

As a purely practical consideration, we wondered "what if the parent
generator was yielded at the time we tried to create the fork?" That
way, the fork would be either be at its first yield point, or have
failed when we tried to resume the parent, but this turned out to be a
profoundly positive re-orientation of the effection architecture to
separate out the concepts of context and executions from the generator
synax entirely which enables all sorts of nice things.

Fundamentally, this change makes effection a function-oriented effects
library rather than a generator-oriented effects library. That is, you
don't _need_ generators to declare a tree of processes, it's just
nicer to do it that way. For example:

```js

enter(function*() {
  yield fork(function*() {
    return 5;
  });
});

```

can now be re-written using the function-equivalent:

```js
enter(({ call }) => {
  call(fork(({ resume }) => resume(5)));
})

The nice effect that this has is that the generator syntax is
implemented as just a plugin to the basic runtime, which means that in
addition to solving the problem at hand, it also [allows you to fork a
control function directly][2], something that had always felt like it
should work, but annoyingly didn't.

This feels like the right direction to take it, because it is the
solution to several seemingly unrelated problems, and it even feels as
though it could provide a pathway to an [asynchronous halt
operation][3] by making not just `fork` but also `halt` an operation.

Finally, because structural concurrency is implemented ultimately as
operations that do nothing more than manipulate the execution context,
other structured effects (state, messaging, etc...) can be implemented
in user space. Using this library, I was able to implement messaging
operations based on the [send-and-receive][4] branch completely in
user-space, and in which both `send()` and `receive()` were
operations.

You can now seemlessly move between function syntax and generator
syntax since they are all just operations that run inside the bounds
of an execution context.

For example, here is a "hybrid" version of the previous example using
a mixture of function syntax and generator syntax.

```js
enter(({ call }) => {
  call(fork(function*() {
    return 5;
  });
})
```

From the perspective of the JavaScript runtime, execution is still
completely synchronous internally, and the code is much, much simpler.

Details
-------

What was formerly known as a `Fork` and was coupled to a generator,
has now become two objects, an `ExecutionContext` and a
`ControlFuction`. Where the `ExecutionContext` tracks the state of the
frame of execution and the control function calls `resume`, `fail`, or
`halt` at the appropriate time. Every operation is eventually
resolved to a control function.

The key new primitive that is introduced is the `call` function, which
is passed into the control function with each execution context:

```js
enter(({ call }) => {
  call(function*() {
    yield timeout(100);
    return 5;
  });
});
```

This provisions a new execution context as a child of the current
context, looks up the operation passsed in order to find its control
function, and then enters that child context. Essentially, this is an
implementation of a call stack, except instead of it being a stack,
it's a tree.

Most effection code however, will never need to know about `call`,
since most operations do not actually create any children. But for
those that do such as the generator operation and the `fork`
operation, it allows them to be treated uniformly.

By default, child contexts created by `call` are considered "linked"
in the sense that if they fail, they cause the parent to fail, or if
they halt, then they cause their parent to fail because a required
child was. However, by passing callbacks to the `call` invocation, you
can override this behavior to "handle" an error, presumably by either
catching it or propagating it.

```js
call(operation, { fail: (e) => console.log('caught error: ', e) });
```
As mentioned previously, the default behavior is to cause the parent
context to also fail.

It's worth noting that with this power, it is possible to implement
completely in user-space try/catch/finally as operations, for example:

```js
function*() {
  yield tcf({
    try: () => do something,
    catch: (e) => console.log('error = ', e),
    finally: () => teardown()
  })
}
```

Open Questions
______________

- [ ] I'm not sure if the interface for the controls is good. Right
  now, the full signature of a control function is `({ resume, fail,
  ensure call, context })`, but part of be feels like it should be
  more like a promise: `({ resolve, reject, ensure, call, context
  })`. Although maybe even using pseudo reserved words would be better
  to re-enforce the concepts like `({ $continue, $throw, $call,
  $finally })`.

[1]: #26
[2]: #33
[3]: #35
[4]: #49
cowboyd added a commit that referenced this issue Jan 15, 2020
Motivation
----------

Originally, this change came out of problems with sequencing a
fork. Specifically, if there was an error inside of fork, and so it
tried to throw() inside of its parent, then the error was hidden by
the fact that [its parent generator was already running]. This was
symptomatic of a deeper problem of the generator syntax being too
tightly coupled to the underlying concept of the execution of a
sequence of steps, and also of the concept of synchronous and
asynchronous execution being too coupled.

As a purely practical consideration, we wondered "what if the parent
generator was yielded at the time we tried to create the fork?" That
way, the fork would be either be at its first yield point, or have
failed when we tried to resume the parent, but this turned out to be a
profoundly positive re-orientation of the effection architecture to
separate out the concepts of context and executions from the generator
synax entirely which enables all sorts of nice things.

Fundamentally, this change makes effection a function-oriented effects
library rather than a generator-oriented effects library. That is, you
don't _need_ generators to declare a tree of processes, it's just
nicer to do it that way. For example:

```js

enter(function*() {
  yield fork(function*() {
    return 5;
  });
});

```

can now be re-written using the function-equivalent:

```js
enter(({ call }) => {
  call(fork(({ resume }) => resume(5)));
})

The nice effect that this has is that the generator syntax is
implemented as just a plugin to the basic runtime, which means that in
addition to solving the problem at hand, it also [allows you to fork a
control function directly][2], something that had always felt like it
should work, but annoyingly didn't.

This feels like the right direction to take it, because it is the
solution to several seemingly unrelated problems, and it even feels as
though it could provide a pathway to an [asynchronous halt
operation][3] by making not just `fork` but also `halt` an operation.

Finally, because structural concurrency is implemented ultimately as
operations that do nothing more than manipulate the execution context,
other structured effects (state, messaging, etc...) can be implemented
in user space. Using this library, I was able to implement messaging
operations based on the [send-and-receive][4] branch completely in
user-space, and in which both `send()` and `receive()` were
operations.

You can now seemlessly move between function syntax and generator
syntax since they are all just operations that run inside the bounds
of an execution context.

For example, here is a "hybrid" version of the previous example using
a mixture of function syntax and generator syntax.

```js
enter(({ call }) => {
  call(fork(function*() {
    return 5;
  });
})
```

From the perspective of the JavaScript runtime, execution is still
completely synchronous internally, and the code is much, much simpler.

Details
-------

What was formerly known as a `Fork` and was coupled to a generator,
has now become two objects, an `ExecutionContext` and a
`ControlFuction`. Where the `ExecutionContext` tracks the state of the
frame of execution and the control function calls `resume`, `fail`, or
`halt` at the appropriate time. Every operation is eventually
resolved to a control function.

The key new primitive that is introduced is the `call` function, which
is passed into the control function with each execution context:

```js
enter(({ call }) => {
  call(function*() {
    yield timeout(100);
    return 5;
  });
});
```

This provisions a new execution context as a child of the current
context, looks up the operation passsed in order to find its control
function, and then enters that child context. Essentially, this is an
implementation of a call stack, except instead of it being a stack,
it's a tree.

Most effection code however, will never need to know about `call`,
since most operations do not actually create any children. But for
those that do such as the generator operation and the `fork`
operation, it allows them to be treated uniformly.

By default, child contexts created by `call` are considered "linked"
in the sense that if they fail, they cause the parent to fail, or if
they halt, then they cause their parent to fail because a required
child was. However, by passing callbacks to the `call` invocation, you
can override this behavior to "handle" an error, presumably by either
catching it or propagating it.

```js
call(operation, { fail: (e) => console.log('caught error: ', e) });
```
As mentioned previously, the default behavior is to cause the parent
context to also fail.

It's worth noting that with this power, it is possible to implement
completely in user-space try/catch/finally as operations, for example:

```js
function*() {
  yield tcf({
    try: () => do something,
    catch: (e) => console.log('error = ', e),
    finally: () => teardown()
  })
}
```

Open Questions
______________

- [ ] I'm not sure if the interface for the controls is good. Right
  now, the full signature of a control function is `({ resume, fail,
  ensure, call, context })`, but part of be feels like it should be
  more like a promise: `({ resolve, reject, ensure, call, context
  })`. Although maybe even using pseudo reserved words would be better
  to re-enforce the concepts like `({ $continue, $throw, $call,
  $finally })`.

[1]: #26
[2]: #33
[3]: #35
[4]: #49
cowboyd added a commit that referenced this issue Jan 15, 2020
Motivation
----------

Originally, this change came out of problems with sequencing a
fork. Specifically, if there was an error inside of fork, and so it
tried to throw() inside of its parent, then the error was hidden by
the fact that [its parent generator was already running]. This was
symptomatic of a deeper problem of the generator syntax being too
tightly coupled to the underlying concept of the execution of a
sequence of steps, and also of the concept of synchronous and
asynchronous execution being too coupled.

As a purely practical consideration, we wondered "what if the parent
generator was yielded at the time we tried to create the fork?" That
way, the fork would be either be at its first yield point, or have
failed when we tried to resume the parent, but this turned out to be a
profoundly positive re-orientation of the effection architecture to
separate out the concepts of context and executions from the generator
synax entirely which enables all sorts of nice things.

Fundamentally, this change makes effection a function-oriented effects
library rather than a generator-oriented effects library. That is, you
don't _need_ generators to declare a tree of processes, it's just
nicer to do it that way. For example:

```js

enter(function*() {
  yield fork(function*() {
    return 5;
  });
});

```

can now be re-written using the function-equivalent:

```js
enter(({ call }) => {
  call(fork(({ resume }) => resume(5)));
})

The nice effect that this has is that the generator syntax is
implemented as just a plugin to the basic runtime, which means that in
addition to solving the problem at hand, it also [allows you to fork a
control function directly][2], something that had always felt like it
should work, but annoyingly didn't.

This feels like the right direction to take it, because it is the
solution to several seemingly unrelated problems, and it even feels as
though it could provide a pathway to an [asynchronous halt
operation][3] by making not just `fork` but also `halt` an operation.

Finally, because structural concurrency is implemented ultimately as
operations that do nothing more than manipulate the execution context,
other structured effects (state, messaging, etc...) can be implemented
in user space. Using this library, I was able to implement messaging
operations based on the [send-and-receive][4] branch completely in
user-space, and in which both `send()` and `receive()` were
operations.

You can now seemlessly move between function syntax and generator
syntax since they are all just operations that run inside the bounds
of an execution context.

For example, here is a "hybrid" version of the previous example using
a mixture of function syntax and generator syntax.

```js
enter(({ call }) => {
  call(fork(function*() {
    return 5;
  });
})
```

From the perspective of the JavaScript runtime, execution is still
completely synchronous internally, and the code is much, much simpler.

Details
-------

What was formerly known as a `Fork` and was coupled to a generator,
has now become two objects, an `ExecutionContext` and a
`ControlFuction`. Where the `ExecutionContext` tracks the state of the
frame of execution and the control function calls `resume`, `fail`, or
`halt` at the appropriate time. Every operation is eventually
resolved to a control function.

The key new primitive that is introduced is the `call` function, which
is passed into the control function with each execution context:

```js
enter(({ call }) => {
  call(function*() {
    yield timeout(100);
    return 5;
  });
});
```

This provisions a new execution context as a child of the current
context, looks up the operation passsed in order to find its control
function, and then enters that child context. Essentially, this is an
implementation of a call stack, except instead of it being a stack,
it's a tree.

Most effection code however, will never need to know about `call`,
since most operations do not actually create any children. But for
those that do such as the generator operation and the `fork`
operation, it allows them to be treated uniformly.

By default, child contexts created by `call` are considered "linked"
in the sense that if they fail, they cause the parent to fail, or if
they halt, then they cause their parent to fail because a required
child was. However, by passing callbacks to the `call` invocation, you
can override this behavior to "handle" an error, presumably by either
catching it or propagating it.

```js
call(operation, { fail: (e) => console.log('caught error: ', e) });
```
As mentioned previously, the default behavior is to cause the parent
context to also fail.

It's worth noting that with this power, it is possible to implement
completely in user-space try/catch/finally as operations, for example:

```js
function*() {
  yield tcf({
    try: () => do something,
    catch: (e) => console.log('error = ', e),
    finally: () => teardown()
  })
}
```

Open Questions
______________

- [ ] I'm not sure if the interface for the controls is good. Right
  now, the full signature of a control function is `({ resume, fail,
  ensure, call, context })`, but part of be feels like it should be
  more like a promise: `({ resolve, reject, ensure, call, context
  })`. Although maybe even using pseudo reserved words would be better
  to re-enforce the concepts like `({ $continue, $throw, $call,
  $finally })`.

[1]: #26
[2]: #33
[3]: #35
[4]: #49
cowboyd added a commit that referenced this issue Jan 15, 2020
Motivation
----------

Originally, this change came out of problems with sequencing a
fork. Specifically, if there was an error inside of fork, and so it
tried to throw() inside of its parent, then the error was hidden by
the fact that [its parent generator was already running]. This was
symptomatic of a deeper problem of the generator syntax being too
tightly coupled to the underlying concept of the execution of a
sequence of steps, and also of the concept of synchronous and
asynchronous execution being too coupled.

As a purely practical consideration, we wondered "what if the parent
generator was yielded at the time we tried to create the fork?" That
way, the fork would be either be at its first yield point, or have
failed when we tried to resume the parent, but this turned out to be a
profoundly positive re-orientation of the effection architecture to
separate out the concepts of context and executions from the generator
synax entirely which enables all sorts of nice things.

Fundamentally, this change makes effection a function-oriented effects
library rather than a generator-oriented effects library. That is, you
don't _need_ generators to declare a tree of processes, it's just
nicer to do it that way. For example:

```js

enter(function*() {
  yield fork(function*() {
    return 5;
  });
});

```

can now be re-written using the function-equivalent:

```js
enter(({ call }) => {
  call(fork(({ resume }) => resume(5)));
})

The nice effect that this has is that the generator syntax is
implemented as just a plugin to the basic runtime, which means that in
addition to solving the problem at hand, it also [allows you to fork a
control function directly][2], something that had always felt like it
should work, but annoyingly didn't.

This feels like the right direction to take it, because it is the
solution to several seemingly unrelated problems, and it even feels as
though it could provide a pathway to an [asynchronous halt
operation][3] by making not just `fork` but also `halt` an operation.

Finally, because structural concurrency is implemented ultimately as
operations that do nothing more than manipulate the execution context,
other structured effects (state, messaging, etc...) can be implemented
in user space. Using this library, I was able to implement messaging
operations based on the [send-and-receive][4] branch completely in
user-space, and in which both `send()` and `receive()` were
operations.

You can now seemlessly move between function syntax and generator
syntax since they are all just operations that run inside the bounds
of an execution context.

For example, here is a "hybrid" version of the previous example using
a mixture of function syntax and generator syntax.

```js
enter(({ call }) => {
  call(fork(function*() {
    return 5;
  });
})
```

From the perspective of the JavaScript runtime, execution is still
completely synchronous internally, and the code is much, much simpler.

Details
-------

What was formerly known as a `Fork` and was coupled to a generator,
has now become two objects, an `ExecutionContext` and a
`ControlFuction`. Where the `ExecutionContext` tracks the state of the
frame of execution and the control function calls `resume`, `fail`, or
`halt` at the appropriate time. Every operation is eventually
resolved to a control function.

The key new primitive that is introduced is the `call` function, which
is passed into the control function with each execution context:

```js
enter(({ call }) => {
  call(function*() {
    yield timeout(100);
    return 5;
  });
});
```

This provisions a new execution context as a child of the current
context, looks up the operation passsed in order to find its control
function, and then enters that child context. Essentially, this is an
implementation of a call stack, except instead of it being a stack,
it's a tree.

Most effection code however, will never need to know about `call`,
since most operations do not actually create any children. But for
those that do such as the generator operation and the `fork`
operation, it allows them to be treated uniformly.

By default, child contexts created by `call` are considered "linked"
in the sense that if they fail, they cause the parent to fail, or if
they halt, then they cause their parent to fail because a required
child was. However, by passing callbacks to the `call` invocation, you
can override this behavior to "handle" an error, presumably by either
catching it or propagating it.

```js
call(operation, { fail: (e) => console.log('caught error: ', e) });
```
As mentioned previously, the default behavior is to cause the parent
context to also fail.

It's worth noting that with this power, it is possible to implement
completely in user-space try/catch/finally as operations, for example:

```js
function*() {
  yield tcf({
    try: () => do something,
    catch: (e) => console.log('error = ', e),
    finally: () => teardown()
  })
}
```

Open Questions
______________

- [ ] I'm not sure if the interface for the controls is good. Right
  now, the full signature of a control function is `({ resume, fail,
  ensure, call, context })`, but part of be feels like it should be
  more like a promise: `({ resolve, reject, ensure, call, context
  })`. Although maybe even using pseudo reserved words would be better
  to re-enforce the concepts like `({ $continue, $throw, $call,
  $finally })`.

[1]: #26
[2]: #33
[3]: #35
[4]: #49
cowboyd added a commit that referenced this issue Jan 15, 2020
Motivation
----------

Originally, this change came out of problems with sequencing a
fork. Specifically, if there was an error inside of fork, and so it
tried to throw() inside of its parent, then the error was hidden by
the fact that [its parent generator was already running]. This was
symptomatic of a deeper problem of the generator syntax being too
tightly coupled to the underlying concept of the execution of a
sequence of steps, and also of the concept of synchronous and
asynchronous execution being too coupled.

As a purely practical consideration, we wondered "what if the parent
generator was yielded at the time we tried to create the fork?" That
way, the fork would be either be at its first yield point, or have
failed when we tried to resume the parent, but this turned out to be a
profoundly positive re-orientation of the effection architecture to
separate out the concepts of context and executions from the generator
synax entirely which enables all sorts of nice things.

Fundamentally, this change makes effection a function-oriented effects
library rather than a generator-oriented effects library. That is, you
don't _need_ generators to declare a tree of processes, it's just
nicer to do it that way. For example:

```js

enter(function*() {
  yield fork(function*() {
    return 5;
  });
});

```

can now be re-written using the function-equivalent:

```js
enter(({ call }) => {
  call(fork(({ resume }) => resume(5)));
})

The nice effect that this has is that the generator syntax is
implemented as just a plugin to the basic runtime, which means that in
addition to solving the problem at hand, it also [allows you to fork a
control function directly][2], something that had always felt like it
should work, but annoyingly didn't.

This feels like the right direction to take it, because it is the
solution to several seemingly unrelated problems, and it even feels as
though it could provide a pathway to an [asynchronous halt
operation][3] by making not just `fork` but also `halt` an operation.

Finally, because structural concurrency is implemented ultimately as
operations that do nothing more than manipulate the execution context,
other structured effects (state, messaging, etc...) can be implemented
in user space. Using this library, I was able to implement messaging
operations based on the [send-and-receive][4] branch completely in
user-space, and in which both `send()` and `receive()` were
operations.

You can now seemlessly move between function syntax and generator
syntax since they are all just operations that run inside the bounds
of an execution context.

For example, here is a "hybrid" version of the previous example using
a mixture of function syntax and generator syntax.

```js
enter(({ call }) => {
  call(fork(function*() {
    return 5;
  });
})
```

From the perspective of the JavaScript runtime, execution is still
completely synchronous internally, and the code is much, much simpler.

Details
-------

What was formerly known as a `Fork` and was coupled to a generator,
has now become two objects, an `ExecutionContext` and a
`ControlFuction`. Where the `ExecutionContext` tracks the state of the
frame of execution and the control function calls `resume`, `fail`, or
`halt` at the appropriate time. Every operation is eventually
resolved to a control function.

The key new primitive that is introduced is the `call` function, which
is passed into the control function with each execution context:

```js
enter(({ call }) => {
  call(function*() {
    yield timeout(100);
    return 5;
  });
});
```

This provisions a new execution context as a child of the current
context, looks up the operation passsed in order to find its control
function, and then enters that child context. Essentially, this is an
implementation of a call stack, except instead of it being a stack,
it's a tree.

Most effection code however, will never need to know about `call`,
since most operations do not actually create any children. But for
those that do such as the generator operation and the `fork`
operation, it allows them to be treated uniformly.

By default, child contexts created by `call` are considered "linked"
in the sense that if they fail, they cause the parent to fail, or if
they halt, then they cause their parent to fail because a required
child was. However, by passing callbacks to the `call` invocation, you
can override this behavior to "handle" an error, presumably by either
catching it or propagating it.

```js
call(operation, { fail: (e) => console.log('caught error: ', e) });
```
As mentioned previously, the default behavior is to cause the parent
context to also fail.

It's worth noting that with this power, it is possible to implement
completely in user-space try/catch/finally as operations, for example:

```js
function*() {
  yield tcf({
    try: () => do something,
    catch: (e) => console.log('error = ', e),
    finally: () => teardown()
  })
}
```

Open Questions
______________

- [ ] I'm not sure if the interface for the controls is good. Right
  now, the full signature of a control function is `({ resume, fail,
  ensure, call, context })`, but part of be feels like it should be
  more like a promise: `({ resolve, reject, ensure, call, context
  })`. Although maybe even using pseudo reserved words would be better
  to re-enforce the concepts like `({ $continue, $throw, $call,
  $finally })`.

[1]: #26
[2]: #33
[3]: #35
[4]: #49
cowboyd added a commit that referenced this issue Jan 23, 2020
Motivation
----------

Originally, this change came out of problems with sequencing a
fork. Specifically, if there was an error inside of fork, and so it
tried to throw() inside of its parent, then the error was hidden by
the fact that [its parent generator was already running]. This was
symptomatic of a deeper problem of the generator syntax being too
tightly coupled to the underlying concept of the execution of a
sequence of steps, and also of the concept of synchronous and
asynchronous execution being too coupled.

As a purely practical consideration, we wondered "what if the parent
generator was yielded at the time we tried to create the fork?" That
way, the fork would be either be at its first yield point, or have
failed when we tried to resume the parent, but this turned out to be a
profoundly positive re-orientation of the effection architecture to
separate out the concepts of context and executions from the generator
synax entirely which enables all sorts of nice things.

Fundamentally, this change makes effection a function-oriented effects
library rather than a generator-oriented effects library. That is, you
don't _need_ generators to declare a tree of processes, it's just
nicer to do it that way. For example:

```js

enter(function*() {
  yield fork(function*() {
    return 5;
  });
});

```

can now be re-written using the function-equivalent:

```js
enter(({ call }) => {
  call(fork(({ resume }) => resume(5)));
})

The nice effect that this has is that the generator syntax is
implemented as just a plugin to the basic runtime, which means that in
addition to solving the problem at hand, it also [allows you to fork a
control function directly][2], something that had always felt like it
should work, but annoyingly didn't.

This feels like the right direction to take it, because it is the
solution to several seemingly unrelated problems, and it even feels as
though it could provide a pathway to an [asynchronous halt
operation][3] by making not just `fork` but also `halt` an operation.

Finally, because structural concurrency is implemented ultimately as
operations that do nothing more than manipulate the execution context,
other structured effects (state, messaging, etc...) can be implemented
in user space. Using this library, I was able to implement messaging
operations based on the [send-and-receive][4] branch completely in
user-space, and in which both `send()` and `receive()` were
operations.

You can now seemlessly move between function syntax and generator
syntax since they are all just operations that run inside the bounds
of an execution context.

For example, here is a "hybrid" version of the previous example using
a mixture of function syntax and generator syntax.

```js
enter(({ call }) => {
  call(fork(function*() {
    return 5;
  });
})
```

From the perspective of the JavaScript runtime, execution is still
completely synchronous internally, and the code is much, much simpler.

Details
-------

What was formerly known as a `Fork` and was coupled to a generator,
has now become two objects, an `ExecutionContext` and a
`ControlFuction`. Where the `ExecutionContext` tracks the state of the
frame of execution and the control function calls `resume`, `fail`, or
`halt` at the appropriate time. Every operation is eventually
resolved to a control function.

The key new primitive that is introduced is the `call` function, which
is passed into the control function with each execution context:

```js
enter(({ call }) => {
  call(function*() {
    yield timeout(100);
    return 5;
  });
});
```

This provisions a new execution context as a child of the current
context, looks up the operation passsed in order to find its control
function, and then enters that child context. Essentially, this is an
implementation of a call stack, except instead of it being a stack,
it's a tree.

Most effection code however, will never need to know about `call`,
since most operations do not actually create any children. But for
those that do such as the generator operation and the `fork`
operation, it allows them to be treated uniformly.

By default, child contexts created by `call` are considered "linked"
in the sense that if they fail, they cause the parent to fail, or if
they halt, then they cause their parent to fail because a required
child was. However, by passing callbacks to the `call` invocation, you
can override this behavior to "handle" an error, presumably by either
catching it or propagating it.

```js
call(operation, { fail: (e) => console.log('caught error: ', e) });
```
As mentioned previously, the default behavior is to cause the parent
context to also fail.

It's worth noting that with this power, it is possible to implement
completely in user-space try/catch/finally as operations, for example:

```js
function*() {
  yield tcf({
    try: () => do something,
    catch: (e) => console.log('error = ', e),
    finally: () => teardown()
  })
}
```

Open Questions
______________

- [ ] I'm not sure if the interface for the controls is good. Right
  now, the full signature of a control function is `({ resume, fail,
  ensure, call, context })`, but part of be feels like it should be
  more like a promise: `({ resolve, reject, ensure, call, context
  })`. Although maybe even using pseudo reserved words would be better
  to re-enforce the concepts like `({ $continue, $throw, $call,
  $finally })`.

[1]: #26
[2]: #33
[3]: #35
[4]: #49
@jnicklas jnicklas added this to the v2 milestone Nov 2, 2020
@jnicklas
Copy link
Collaborator

jnicklas commented Mar 4, 2021

This is now implemented in v2.

@jnicklas jnicklas closed this as completed Mar 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants