-
Notifications
You must be signed in to change notification settings - Fork 24
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
Labels
enhancement
New feature or request
Milestone
Comments
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
Somewhat related to this: it should also be possible to yield to a halt. Ideally, I think, 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
This is now implemented in v2. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.The text was updated successfully, but these errors were encountered: