Last active
August 28, 2022 23:29
-
-
Save Mando75/f6ec61b39a39ce72043df47e4cfc6d32 to your computer and use it in GitHub Desktop.
Suspended Execution using generator example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type ValidatorGenerator = AsyncGenerator<Error, number> | |
type OperationGenerator = AsyncGenerator<ValidatorGenerator, { num: number }, number> | |
/** | |
* This function yields an Error if the input is invalid | |
* or returns the data if it is ok | |
* @param number | |
*/ | |
function* validator(number: number): ValidatorGenerator { | |
if (number % 2 === 0) { | |
yield Error(`${number} is a multiple of 2`) | |
} | |
return number | |
} | |
/** | |
* An example operation that needs to validate some data prior to performing some action | |
* | |
* This validator can return either an Error or a number. | |
* By yielding the result of the validation, we allow the top-level | |
* error handler to stop execution if the result is an error, and only | |
* resume the operation if the data is valid. | |
* | |
* Our example operation will multiply any valid inputs by 5 | |
* @param input | |
*/ | |
async function* exampleOperation(input: number): OperationGenerator { | |
const validatedInput = yield validator(input) | |
return {num: validatedInput * 5 } | |
} | |
type Operation = typeof exampleOperation | |
/** | |
* Top level function to run our operation within the context of the suspension boundary. | |
*/ | |
async function runner(operation: Operation, initialValue: number) { | |
// Initialize the operation | |
const result = exampleOperation(initialValue) | |
// Run it in context | |
return runContext(result, initialValue) | |
} | |
/** | |
* Iterates through an operation and recursively checks for | |
* validation errors. In this example, we return the error instead of providing | |
* the value to the operation, but theoretically this could be used to short circuit something like a | |
* request handler | |
* @param gen | |
* @param nextVal | |
*/ | |
async function runContext(gen: OperationGenerator, nextVal: number): Promise<{ num: number } | Error> { | |
const step = await gen.next(nextVal) | |
// No validation, we can just return the value | |
if (step.done) { | |
return step.value | |
} | |
// Handle iterating through validation | |
const validationResult = await step.value.next() | |
// Valid input, continue execution of top-level iterator | |
if (validationResult.done) { | |
return runContext(gen, validationResult.value) | |
} | |
// invalid input, return an error | |
return validationResult.value | |
} | |
/** | |
* Our expected result should be that odd numbers are multiplied by 5 | |
* and even numbers are replaced with error messages | |
*/ | |
function exampleRunner() { | |
const values = [1, 2, 3, 4, 5, 6, 7, 8, 9] | |
return Promise.all(values.map((value) => runner(exampleRunner, value))) | |
} | |
exampleRunner().then(console.log) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I'm working on a more reliable implementation of this using the Either monad. From there I might try to extract the actual generator comprehension into a separate package for easier pattern re-usage https://github.com/Mando75/either-suspend