Skip to content

Commit

Permalink
fix: rewrite to use arbitrary arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
boneskull committed Aug 23, 2024
1 parent a0f4323 commit fc4d0b2
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 86 deletions.
38 changes: 18 additions & 20 deletions packages/trampoline/src/trampoline.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
/* eslint-disable @jessie.js/safe-await-separator */
/**
* @import {ThunkFn, SyncTrampolineGeneratorFn, TrampolineGeneratorFn, AsyncTrampolineGeneratorFn, SyncThunkFn } from './types.js'
* @import {AnyGenerator, SyncTrampolineGeneratorFn, AsyncTrampolineGeneratorFn } from './types.js'
*/

/**
* Trampoline on {@link TrampolineGeneratorFn generatorFn} with a synchronous {@link SyncThunkFn thunk}.
* Trampoline on {@link TrampolineGeneratorFn generatorFn} synchronously.
*
* @template TInitial Type of the initial value passed to the `generatorFn`
* @template [TArg=TInitial] Type of the argument passed to the `thunkFn`
* @template [TResult=TArg] Result of the `thunkFn` _and_ the return value of the `generatorFn`
* @param {SyncTrampolineGeneratorFn<TInitial, TArg, TResult>} generatorFn Generator-returning function accepting a thunk and optionally an initial value
* @param {SyncThunkFn<TArg, TResult>} thunk Synchronous thunk which `generatorFn` should call
* @param {TInitial} initial Initial value
* @template {readonly any[]} [TArgs=unknown[]] Parameters for `generatorFn`
* @template [TResult=unknown] Type of the return value of the `generatorFn`
* @template {Generator<any, any, TResult>} [TGenerator=Generator] Type of the generator function
* @param {SyncTrampolineGeneratorFn<TGenerator, TArgs, TResult>} generatorFn Generator-returning function accepting a thunk and optionally an initial value
* @param {TArgs} args Initial args
* @returns {TResult}
*/
export function syncTrampoline(generatorFn, thunk, initial) {
const iterator = generatorFn(thunk, initial);
export function syncTrampoline(generatorFn, ...args) {
const iterator = generatorFn(...args);
let result = iterator.next();
while (!result.done) {
result = iterator.next(result.value);
Expand All @@ -24,18 +23,17 @@ export function syncTrampoline(generatorFn, thunk, initial) {
}

/**
* Trampoline on {@link TrampolineGeneratorFn generatorFn} with a synchronous _or_ asynchronous {@link ThunkFn thunk}.
* Trampoline on {@link TrampolineGeneratorFn generatorFn} asynchronously.
*
* @template TInitial Type of the initial value passed to the `generatorFn`
* @template [TArg=TInitial] Type of the argument passed to the `thunkFn`
* @template [TResult=TArg] Result of `thunkFn` _and_ the return value of the `generatorFn`
* @param {TrampolineGeneratorFn<TInitial, TArg, TResult>} generatorFn Generator-returning function accepting a thunk and optionally an initial value
* @param {ThunkFn<TArg, TResult>} thunk Thunk function
* @param {TInitial} initial Initial value passed to `generatorFn`
* @returns {Promise<Awaited<TResult>>} Final value of generator
* @template {readonly any[]} [TArgs=unknown[]] Parameters for `generatorFn`
* @template [TResult=unknown] Type of the return value of the `generatorFn`
* @template {Generator<any, any, TResult>} [TGenerator=Generator] Type of the generator function
* @param {AsyncTrampolineGeneratorFn<TGenerator, TArgs, TResult>} generatorFn Generator-returning function accepting a thunk and optionally an initial value
* @param {TArgs} args Initial args
* @returns {Promise<TResult>}
*/
export async function trampoline(generatorFn, thunk, initial) {
const iterator = generatorFn(thunk, initial);
export async function trampoline(generatorFn, ...args) {
const iterator = generatorFn(...args);
let result = iterator.next();
while (!result.done) {
// eslint-disable-next-line no-await-in-loop
Expand Down
52 changes: 21 additions & 31 deletions packages/trampoline/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
/**
* A {@link TrampolineGeneratorFn} will yield the result of calling this
* function
*/
export type ThunkFn<TArg, TResult = TArg> = (arg: TArg) => TResult;

/**
* 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;

export type AnyGenerator = Generator<any, any, any>;
/**
* A function type that represents a generator function for trampolining.
*
Expand All @@ -23,10 +11,14 @@ export type SyncThunkFn<TArg, TResult = TArg> =
* @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>>;
export type AsyncTrampolineGeneratorFn<
TGenerator extends AnyGenerator,
TArgs extends readonly any[] = unknown[],
TResult = unknown,
> =
TGenerator extends Generator<infer T, infer TNext>
? (...args: TArgs) => Generator<T, Awaited<TNext>, Awaited<TResult>>
: never;

/**
* A function type that represents a synchronous generator function for
Expand All @@ -35,20 +27,18 @@ export type TrampolineGeneratorFn<TInitial, TArg = TInitial, TResult = TArg> = (
* 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.
* @template TArgs - The type of the arguments passed to the generator function.
* Defaults to `unknown[]`.
* @template TResult - The type of the result produced by the generator function.
* Defaults to `unknown`.
* @param args - The arguments passed to the generator function.
* @returns A generator that yields results of type `TResult`.
*/
export type SyncTrampolineGeneratorFn<
TInitial,
TArg = TInitial,
TResult = TArg,
> = (
thunk: ThunkFn<TArg, TResult>,
initial: TInitial,
) => Generator<TResult, TResult, TResult>;
TGenerator extends AnyGenerator,
TArgs extends readonly any[] = unknown[],
TResult = unknown,
> =
TGenerator extends Generator<infer T, infer TNext>
? (...args: TArgs) => Generator<T, TNext, TResult>
: never;
12 changes: 4 additions & 8 deletions packages/trampoline/test/trampoline-example.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
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
*/
Expand Down Expand Up @@ -51,7 +47,7 @@ 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 {(arg: string) => TResult} thunk
* @param {string} filename
* @returns {Generator<TResult, string[], string[]>}
*/
Expand All @@ -67,17 +63,17 @@ function* loadRecursive(thunk, filename) {

const expected = ['b', 'c', 'c', 'd', 'e', 'f', 'g', 'e', 'f', 'g'];

test('asynchronous execution', async t => {
test('asynchronous execution - example code', async t => {
const asyncResult = await trampoline(loadRecursive, findImportsAsync, 'a');
t.deepEqual(asyncResult, expected);
});

test('asynchronous execution w/ sync thunk', async t => {
test('asynchronous execution w/ sync thunk - example code', async t => {
const asyncResult = await trampoline(loadRecursive, findImportsSync, 'a');
t.deepEqual(asyncResult, expected);
});

test('synchronous execution', t => {
test('synchronous execution - example code', t => {
const syncResult = syncTrampoline(loadRecursive, findImportsSync, 'a');
t.deepEqual(syncResult, expected);
});
36 changes: 21 additions & 15 deletions packages/trampoline/test/trampoline.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
/* eslint-disable no-redeclare */
import { expectAssignable, expectNever, expectNotType, expectType } from 'tsd';
import { expectAssignable, expectType } from 'tsd';
import { trampoline, syncTrampoline } from '../src/trampoline.js';
import {
SyncThunkFn,
SyncTrampolineGeneratorFn,
ThunkFn,
TrampolineGeneratorFn,
AsyncTrampolineGeneratorFn,
} from '../src/types.js';

function syncHook(x: number): number {
Expand All @@ -17,21 +15,29 @@ async function asyncHook(x: number): Promise<string> {
return `${x}`;
}

expectAssignable<ThunkFn<number, number>>(syncHook);

expectAssignable<ThunkFn<number, Promise<string>>>(asyncHook);

function* simple<
TResult extends string | Promise<string>,
Thunk extends ThunkFn<string, TResult>,
>(thunk: Thunk, initial: string): Generator<TResult, string, string> {
function* simple<TResult extends string | Promise<string>>(
thunk: (arg: string) => TResult,
initial: string,
): Generator<TResult, string, string> {
const hello = yield thunk(initial);
return `${hello} world`;
}

expectAssignable<TrampolineGeneratorFn<string>>(simple);

expectAssignable<SyncTrampolineGeneratorFn<string>>(simple);
expectAssignable<
AsyncTrampolineGeneratorFn<
ReturnType<typeof simple>,
[(arg: string) => Promise<string>, string],
Promise<string>
>
>(simple);

expectAssignable<
SyncTrampolineGeneratorFn<
ReturnType<typeof simple>,
[(arg: string) => string, string],
string
>
>(simple);

expectType<string>(
syncTrampoline(simple, (str: string) => `${str} cruel`, 'goodbye'),
Expand Down
50 changes: 38 additions & 12 deletions packages/trampoline/test/trampoline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ import test from 'ava';
import { setTimeout } from 'node:timers';
import { syncTrampoline, trampoline } from '../src/trampoline.js';

/**
* @import {ThunkFn} from '../src/types.js'
*/

/**
* @template {number|Promise<number>} TResult
* @param {ThunkFn<number, TResult>} thunk
* @param {(arg: number) => TResult} thunk
* @param {number} [input]
* @returns {Generator<TResult, number, number>}
*/
Expand All @@ -29,6 +25,23 @@ function* operationsWithThunk(thunk, input = 0) {
return result;
}

/**
* @template {number|Promise<number>} TResult
* @param {(arg: number) => TResult} thunk
* @param {number} [input]
* @returns {Generator<TResult, number, number>}
*/
function* operations(thunk, input = 0) {
let result = input * 2; // First operation
result = yield thunk(result); // Call the hook, which can be sync or async
result *= 2; // Operation on the hook's result
// Check if the result is divisible by N, and if so, recurse
while (result < 1000) {
result *= 2;
}
return result;
}

/**
* Synchronous thunk
* @param {number} x
Expand All @@ -48,22 +61,35 @@ async function asyncThunk(x = 0) {
return x + 10;
}

const expectedResult = 2980;
const expectedRecursionResult = 2980;
const expectedResult = 1280;

test('synchronous execution', t => {
test('synchronous execution - recursion', t => {
const syncResult = syncTrampoline(operationsWithThunk, syncThunk, 5);
t.is(syncResult, expectedRecursionResult);
});

test('asynchronous execution w/ sync thunk - recursion', async t => {
const asyncResult = await trampoline(operationsWithThunk, syncThunk, 5);
t.is(asyncResult, expectedRecursionResult);
});

test('asynchronous execution - recursion', async t => {
const asyncResult = await trampoline(operationsWithThunk, asyncThunk, 5);
t.is(asyncResult, expectedRecursionResult);
});

test('synchronous execution', t => {
const syncResult = syncTrampoline(operations, syncThunk, 5);
t.is(syncResult, expectedResult);
t.pass();
});

test('asynchronous execution w/ sync thunk', async t => {
const asyncResult = await trampoline(operationsWithThunk, syncThunk, 5);
const asyncResult = await trampoline(operations, syncThunk, 5);
t.is(asyncResult, expectedResult);
t.pass();
});

test('asynchronous execution', async t => {
const asyncResult = await trampoline(operationsWithThunk, asyncThunk, 5);
const asyncResult = await trampoline(operations, syncThunk, 5);
t.is(asyncResult, expectedResult);
t.pass();
});

0 comments on commit fc4d0b2

Please sign in to comment.