-
Notifications
You must be signed in to change notification settings - Fork 72
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
354 additions
and
63 deletions.
There are no files selected for viewing
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,86 @@ | ||
# supertramp | ||
# @endo/trampoline | ||
|
||
> Sync and async trampolines, for your pleasure | ||
> Multicolor trampolining using generators (for your pleasure) | ||
## Wat | ||
## Example Usage | ||
|
||
From [Wikipedia](https://en.wikipedia.org/wiki/Trampoline_(computing)): | ||
```js | ||
import {trampoline, syncTrampoline} from '@endo/trampoline'; | ||
|
||
/** | ||
* This function "reads a file synchronously" and returns "a list of its imports" | ||
* | ||
* @param {string} filepath Source file path | ||
* @returns {string[]} List of imports found in source | ||
*/ | ||
const findImportsSync = filepath => { | ||
// read a file, parse it for imports, return a list of import specifiers | ||
// (synchronously) | ||
// ... | ||
}; | ||
|
||
/** | ||
* This function "reads a file asynchronously" and returns "a list of its imports" | ||
* | ||
* @param {string} filepath Source file path | ||
* @returns {Promise<string[]>} List of imports found in source | ||
*/ | ||
const findImportsAsync = async filepath => { | ||
// read a file, parse it for imports, return a list of import specifiers | ||
// (asynchronously) | ||
// ... | ||
}; | ||
|
||
/** | ||
* Recursively crawls a dependency tree to find all dependencies | ||
* | ||
* @template {string[]|Promise<string[]>} TResult | ||
* @param {ThunkFn<string, TResult>} thunk | ||
* @param {string} filename | ||
* @returns {Generator<TResult, string[], string[]>} | ||
*/ | ||
function* loadRecursive(thunk, filename) { | ||
let specifiers = yield thunk(filename); | ||
|
||
// pretend there's some de-duping, caching, | ||
// scrubbing, etc. happening here! | ||
|
||
for (const specifier of specifiers) { | ||
specifiers = [...specifiers, ...(yield* loadRecursive(thunk, specifier))]; | ||
} | ||
return specifiers; | ||
} | ||
|
||
// results are an array of all imports found in some.js' dependency tree | ||
|
||
const asyncResult = await trampoline(loadRecursive, readAsync, './some.js'); | ||
const syncResult = syncTrampoline(loadRecursive, readSync, './some.txt'); | ||
|
||
asyncResult === syncResult; // true | ||
``` | ||
|
||
In the above example, **@endo/trampoline** allows us to re-use the operations in `loadRecursive` for both sync and async execution. An implementation _without_ **@endo/trampoline** would need to duplicate the operations into two (2) discrete recursive functions—a synchronous-colored function and an asynchronous-colored function. Over time, this situation commonly leads to diverging implementations. If that _doesn't_ sound like a big deal for _whatever you're trying to do here_, then you probably don't need **@endo/trampoline**. | ||
|
||
## What is this? | ||
|
||
The pattern exposed by this library—known as [trampolining][]—helps manage control flow in a way that avoids deep recursion and potential stack overflows. It effectively "converts" recursive calls into a loop. This is especially helpful in a language like JavaScript [because reasons][proper-tail-calls]. | ||
|
||
**@endo/trampoline** provides the trampolining pattern, but in such a way that consumer can execute _either_ synchronous _or_ asynchronous operations _paired with operations common to both_. | ||
|
||
In other words, **@endo/trampoline** can help _reduce code duplication_ when recursive operations must be executed _in both sync and async_ contexts. | ||
|
||
## Install | ||
|
||
The usual sort of thing: | ||
|
||
```sh | ||
npm install @endo/trampoline | ||
``` | ||
|
||
## License | ||
|
||
Apache-2.0 | ||
|
||
[trampolining]: https://raganwald.com/2013/03/28/trampolines-in-javascript.html | ||
[proper-tail-calls]: https://www.mgmarlow.com/words/2021-03-27-proper-tail-calls-js/ | ||
|
||
> A trampoline is a loop that iteratively invokes [thunk](https://en.wikipedia.org/wiki/Thunk_(functional_programming))-returning functions ([continuation-passing style](https://en.wikipedia.org/wiki/Continuation-passing_style)). |
This file contains 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
This file contains 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
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,54 @@ | ||
export type ThunkFn<HookArg, HookResult> = (arg: HookArg) => HookResult; | ||
/** | ||
* A {@link TrampolineGeneratorFn} will yield the result of calling this | ||
* function | ||
*/ | ||
export type ThunkFn<TArg, TResult = TArg> = (arg: TArg) => TResult; | ||
|
||
export type TrampolineGeneratorFn< | ||
TInitial, | ||
TArg = TInitial, | ||
TResult = TArg, | ||
Thunk extends ThunkFn<TArg, TResult> = ThunkFn<TArg, TResult>, | ||
> = ( | ||
thunkFn: Thunk, | ||
/** | ||
* A {@link SyncTrampolineGeneratorFn} or {@link TrampolineGeneratorFn} will | ||
* yield the result of calling this function | ||
*/ | ||
export type SyncThunkFn<TArg, TResult = TArg> = | ||
TResult extends Promise<any> ? never : (arg: TArg) => TResult; | ||
|
||
/** | ||
* A function type that represents a generator function for trampolining. | ||
* | ||
* @template TInitial - The type of the initial value. | ||
* @template TArg - The type of the argument passed to the thunk function. | ||
* Defaults to `TInitial`. | ||
* @template TResult - The type of the result produced by the thunk function. | ||
* Defaults to `TArg`. | ||
* @param thunk - The thunk function to be used in the generator. | ||
* @param initial - The initial value to start the generator. | ||
* @returns A generator that yields results of type `TResult`. | ||
*/ | ||
export type TrampolineGeneratorFn<TInitial, TArg = TInitial, TResult = TArg> = ( | ||
thunk: ThunkFn<TArg, TResult>, | ||
initial: TInitial, | ||
) => Generator<TResult, Awaited<TResult>, Awaited<TResult>>; | ||
|
||
/** | ||
* A function type that represents a synchronous generator function for | ||
* trampolining. | ||
* | ||
* This type ensures that the result type (`TResult`) is not a `Promise`. If | ||
* `TResult` extends `Promise`, the type resolves to `never`. | ||
* | ||
* @template TInitial - The type of the initial value. | ||
* @template TArg - The type of the argument passed to the thunk function. | ||
* Defaults to `TInitial`. | ||
* @template TResult - The type of the result produced by the thunk function. | ||
* Defaults to `TArg`. | ||
* @param thunk - The thunk function to be used in the generator. | ||
* @param initial - The initial value to start the generator. | ||
* @returns A generator that yields results of type `TResult`. | ||
*/ | ||
export type SyncTrampolineGeneratorFn< | ||
TInitial, | ||
TArg = TInitial, | ||
TResult = TArg, | ||
Thunk extends ThunkFn<TArg, TResult> = ThunkFn<TArg, TResult>, | ||
> = | ||
TResult extends Promise<any> | ||
? never | ||
: ( | ||
thunkFn: Thunk, | ||
initial: TInitial, | ||
) => Generator<TResult, TResult, TResult>; | ||
> = ( | ||
thunk: ThunkFn<TArg, TResult>, | ||
initial: TInitial, | ||
) => Generator<TResult, TResult, TResult>; |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
b.txt |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
c.txt |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
d.txt |
Empty file.
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
a.txt |
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
/** | ||
* These tests are based on the example from the `README` | ||
* @module | ||
*/ | ||
|
||
import test from 'ava'; | ||
import { syncTrampoline, trampoline } from '../src/trampoline.js'; | ||
|
||
/** | ||
* @import {ThunkFn} from '../src/types.js' | ||
*/ | ||
|
||
/** | ||
* Mapping of filesnames to import specifiers. Trust me | ||
*/ | ||
const sources = /** @type {const} */ ({ | ||
a: ['b', 'c'], | ||
b: ['c', 'd'], | ||
c: ['e'], | ||
e: ['f', 'g'], | ||
}); | ||
|
||
/** | ||
* This function "inspects the source code and returns a list of specifiers which | ||
* need to be imported."" | ||
* | ||
* @param {string} source | ||
* @returns {string[]} | ||
*/ | ||
const findImportsInSource = source => { | ||
return [...(sources[/** @type {keyof typeof sources} */ (source)] || [])]; | ||
}; | ||
|
||
/** | ||
* This function "reads a file synchronously" and returns "a list of its imports" | ||
* | ||
* @param {string} filepath | ||
* @returns {string[]} | ||
*/ | ||
const findImportsSync = filepath => findImportsInSource(filepath); | ||
|
||
/** | ||
* This function "reads a file asynchronously" and returns "a list of its imports" | ||
* | ||
* @param {string} filepath | ||
* @returns {Promise<string[]>} | ||
*/ | ||
const findImportsAsync = async filepath => findImportsInSource(filepath); | ||
|
||
/** | ||
* Recursively crawls a dependency tree to find all dependencies | ||
* | ||
* @template {string[]|Promise<string[]>} TResult | ||
* @param {ThunkFn<string, TResult>} thunk | ||
* @param {string} filename | ||
* @returns {Generator<TResult, string[], string[]>} | ||
*/ | ||
function* loadRecursive(thunk, filename) { | ||
let specifiers = yield thunk(filename); | ||
// pretend there's some de-duping, caching, | ||
// scrubbing, etc. happening here | ||
for (const specifier of specifiers) { | ||
specifiers = [...specifiers, ...(yield* loadRecursive(thunk, specifier))]; | ||
} | ||
return specifiers; | ||
} | ||
|
||
const expected = ['b', 'c', 'c', 'd', 'e', 'f', 'g', 'e', 'f', 'g']; | ||
|
||
test('asynchronous execution', async t => { | ||
const asyncResult = await trampoline(loadRecursive, findImportsAsync, 'a'); | ||
t.deepEqual(asyncResult, expected); | ||
}); | ||
|
||
test('asynchronous execution w/ sync thunk', async t => { | ||
const asyncResult = await trampoline(loadRecursive, findImportsSync, 'a'); | ||
t.deepEqual(asyncResult, expected); | ||
}); | ||
|
||
test('synchronous execution', t => { | ||
const syncResult = syncTrampoline(loadRecursive, findImportsSync, 'a'); | ||
t.deepEqual(syncResult, expected); | ||
}); |
This file contains 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
Oops, something went wrong.