Skip to content

Commit

Permalink
feat: support async/Promise-returning functions
Browse files Browse the repository at this point in the history
  • Loading branch information
kschat committed Apr 26, 2019
1 parent b4d4a42 commit fee31d2
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 23 deletions.
4 changes: 2 additions & 2 deletions src/thunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ export interface Thunk<T> extends Function {

export type ThunkOrValue<T> = T | Thunk<T>;

export type UnwrapThunk<T> = {
0: T extends Thunk<infer U> ? UnwrapThunk<U> : T;
export type UnwrapThunkDeep<T> = {
0: T extends Thunk<infer U> ? UnwrapThunkDeep<U> : T;
}[
T extends ThunkOrValue<T> ? 0 : never
];
Expand Down
51 changes: 39 additions & 12 deletions src/trampoline.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
import { ArgumentTypes } from './types';
import {
Thunk,
UnwrapThunk,
UnwrapThunkDeep,
isThunk,
toThunk,
ThunkOrValue,
} from './thunk';

export type Cont<A extends any[], R> = (...args: A) => Thunk<UnwrapThunk<R>>;
export type UnwrapPromise<T> = T extends Promise<infer U> ? Exclude<U, Promise<T>> : T;

export type Unbox<T> = UnwrapThunkDeep<UnwrapPromise<T>>;

export type Cont<A extends any[], R> = (...args: A) => Thunk<Unbox<R>>;

export interface Trampoline<F extends ((...args: any[]) => any)> {
(...args: ArgumentTypes<F>): UnwrapThunk<ReturnType<F>>;
(...args: ArgumentTypes<F>): Unbox<ReturnType<F>>;
cont: Cont<ArgumentTypes<F>, ReturnType<F>>;
}

export interface TrampolineAsync<F extends ((...args: any[]) => any)> {
(...args: ArgumentTypes<F>): Promise<Unbox<ReturnType<F>>>;
cont: Cont<ArgumentTypes<F>, ReturnType<F>>;
}

export const trampoline = <F extends ((...args: any[]) => any)>(fn: F): Trampoline<F> => {
const trampolineFunction = (...args: ArgumentTypes<F>): UnwrapThunk<ReturnType<F>> => {
let result: ThunkOrValue<ReturnType<F>> = fn(...args);
const cont = (...args: ArgumentTypes<F>) => toThunk(() => fn(...args));

return Object.assign(
(...args: ArgumentTypes<F>): Unbox<ReturnType<F>> => {
let result: ThunkOrValue<ReturnType<F>> = fn(...args);

while (isThunk<ReturnType<F>>(result)) {
result = result();
}

return result;
},
{ cont },
);
};

while (isThunk<ReturnType<F>>(result)) {
result = result();
}
export const trampolineAsync = <F extends ((...args: any[]) => any)>(fn: F): TrampolineAsync<F> => {
const cont = (...args: ArgumentTypes<F>) => toThunk(() => fn(...args));

return result;
};
return Object.assign(
async (...args: ArgumentTypes<F>): Promise<Unbox<ReturnType<F>>> => {
let result: ThunkOrValue<ReturnType<F>> = await fn(...args);

trampolineFunction.cont = (...args: ArgumentTypes<F>) => toThunk(() => fn(...args));
while (isThunk<ReturnType<F>>(result)) {
result = await result();
}

return trampolineFunction;
return result;
},
{ cont },
);
};
16 changes: 8 additions & 8 deletions test/thunk.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
isThunk,
toThunk,
THUNK_SYMBOL,
UnwrapThunk,
UnwrapThunkDeep,
Thunk,
ThunkOrValue,
} from '../src/thunk';
Expand Down Expand Up @@ -32,13 +32,13 @@ describe('thunk', () => {

describe('UnwrapThunk<T>', () => {
it('removes recursively removes all instances of Thunk<T> from T', () => {
typeAssert<IsExact<UnwrapThunk<1>, 1>>(true);
typeAssert<IsExact<UnwrapThunk<Thunk<1>>, 1>>(true);
typeAssert<IsExact<UnwrapThunk<ThunkOrValue<1>>, 1>>(true);
typeAssert<IsExact<UnwrapThunk<ThunkOrValue<Thunk<1>>>, 1>>(true);
typeAssert<IsExact<UnwrapThunk<ThunkOrValue<ThunkOrValue<1> | ThunkOrValue<2>>>, 1 | 2>>(true);
typeAssert<IsExact<UnwrapThunk<Thunk<1 | Thunk<2>>>, 1 | 2>>(true);
typeAssert<IsExact<UnwrapThunk<Thunk<1 | Thunk<2> | ThunkOrValue<3>>>, 1 | 2 | 3>>(true);
typeAssert<IsExact<UnwrapThunkDeep<1>, 1>>(true);
typeAssert<IsExact<UnwrapThunkDeep<Thunk<1>>, 1>>(true);
typeAssert<IsExact<UnwrapThunkDeep<ThunkOrValue<1>>, 1>>(true);
typeAssert<IsExact<UnwrapThunkDeep<ThunkOrValue<Thunk<1>>>, 1>>(true);
typeAssert<IsExact<UnwrapThunkDeep<ThunkOrValue<ThunkOrValue<1> | ThunkOrValue<2>>>, 1 | 2>>(true);
typeAssert<IsExact<UnwrapThunkDeep<Thunk<1 | Thunk<2>>>, 1 | 2>>(true);
typeAssert<IsExact<UnwrapThunkDeep<Thunk<1 | Thunk<2> | ThunkOrValue<3>>>, 1 | 2 | 3>>(true);
});
});
});
15 changes: 14 additions & 1 deletion test/trampoline.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { assert as typeAssert, IsExact, Has } from 'conditional-type-checks';
import { trampoline, isThunk, ThunkOrValue } from '../src';
import { trampoline, isThunk, ThunkOrValue, trampolineAsync } from '../src';
import { ArgumentTypes } from '../src/types';

describe('trampoline', () => {
Expand Down Expand Up @@ -99,5 +99,18 @@ describe('trampoline', () => {
expect(fn()).toBeInstanceOf(Function);
expect(fn()()).toBe('hello');
});

it('supports async functions', async () => {
const sleep = async (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));

const factorial = trampolineAsync(async (n: number, acc: number = 1): Promise<ThunkOrValue<number>> => {
await sleep(10);
return n
? factorial.cont(n - 1, acc * n)
: acc;
});

expect(await factorial(2)).toBe(2);
});
});
});

0 comments on commit fee31d2

Please sign in to comment.