From fee31d26465b16ee60f822d9aa4e551b17dd3281 Mon Sep 17 00:00:00 2001 From: kschat Date: Fri, 26 Apr 2019 14:21:51 -0400 Subject: [PATCH] feat: support async/Promise-returning functions --- src/thunk.ts | 4 ++-- src/trampoline.ts | 51 +++++++++++++++++++++++++++++++---------- test/thunk.spec.ts | 16 ++++++------- test/trampoline.spec.ts | 15 +++++++++++- 4 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/thunk.ts b/src/thunk.ts index b4bff56..d574632 100644 --- a/src/thunk.ts +++ b/src/thunk.ts @@ -7,8 +7,8 @@ export interface Thunk extends Function { export type ThunkOrValue = T | Thunk; -export type UnwrapThunk = { - 0: T extends Thunk ? UnwrapThunk : T; +export type UnwrapThunkDeep = { + 0: T extends Thunk ? UnwrapThunkDeep : T; }[ T extends ThunkOrValue ? 0 : never ]; diff --git a/src/trampoline.ts b/src/trampoline.ts index 714dcf8..d55ff18 100644 --- a/src/trampoline.ts +++ b/src/trampoline.ts @@ -1,31 +1,58 @@ import { ArgumentTypes } from './types'; import { Thunk, - UnwrapThunk, + UnwrapThunkDeep, isThunk, toThunk, ThunkOrValue, } from './thunk'; -export type Cont = (...args: A) => Thunk>; +export type UnwrapPromise = T extends Promise ? Exclude> : T; + +export type Unbox = UnwrapThunkDeep>; + +export type Cont = (...args: A) => Thunk>; export interface Trampoline any)> { - (...args: ArgumentTypes): UnwrapThunk>; + (...args: ArgumentTypes): Unbox>; + cont: Cont, ReturnType>; +} + +export interface TrampolineAsync any)> { + (...args: ArgumentTypes): Promise>>; cont: Cont, ReturnType>; } export const trampoline = any)>(fn: F): Trampoline => { - const trampolineFunction = (...args: ArgumentTypes): UnwrapThunk> => { - let result: ThunkOrValue> = fn(...args); + const cont = (...args: ArgumentTypes) => toThunk(() => fn(...args)); + + return Object.assign( + (...args: ArgumentTypes): Unbox> => { + let result: ThunkOrValue> = fn(...args); + + while (isThunk>(result)) { + result = result(); + } + + return result; + }, + { cont }, + ); +}; - while (isThunk>(result)) { - result = result(); - } +export const trampolineAsync = any)>(fn: F): TrampolineAsync => { + const cont = (...args: ArgumentTypes) => toThunk(() => fn(...args)); - return result; - }; + return Object.assign( + async (...args: ArgumentTypes): Promise>> => { + let result: ThunkOrValue> = await fn(...args); - trampolineFunction.cont = (...args: ArgumentTypes) => toThunk(() => fn(...args)); + while (isThunk>(result)) { + result = await result(); + } - return trampolineFunction; + return result; + }, + { cont }, + ); }; diff --git a/test/thunk.spec.ts b/test/thunk.spec.ts index 98d8b31..f1dfc74 100644 --- a/test/thunk.spec.ts +++ b/test/thunk.spec.ts @@ -3,7 +3,7 @@ import { isThunk, toThunk, THUNK_SYMBOL, - UnwrapThunk, + UnwrapThunkDeep, Thunk, ThunkOrValue, } from '../src/thunk'; @@ -32,13 +32,13 @@ describe('thunk', () => { describe('UnwrapThunk', () => { it('removes recursively removes all instances of Thunk from T', () => { - typeAssert, 1>>(true); - typeAssert>, 1>>(true); - typeAssert>, 1>>(true); - typeAssert>>, 1>>(true); - typeAssert | ThunkOrValue<2>>>, 1 | 2>>(true); - typeAssert>>, 1 | 2>>(true); - typeAssert | ThunkOrValue<3>>>, 1 | 2 | 3>>(true); + typeAssert, 1>>(true); + typeAssert>, 1>>(true); + typeAssert>, 1>>(true); + typeAssert>>, 1>>(true); + typeAssert | ThunkOrValue<2>>>, 1 | 2>>(true); + typeAssert>>, 1 | 2>>(true); + typeAssert | ThunkOrValue<3>>>, 1 | 2 | 3>>(true); }); }); }); diff --git a/test/trampoline.spec.ts b/test/trampoline.spec.ts index 6fb54a4..00f596f 100644 --- a/test/trampoline.spec.ts +++ b/test/trampoline.spec.ts @@ -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', () => { @@ -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 => new Promise((resolve) => setTimeout(resolve, ms)); + + const factorial = trampolineAsync(async (n: number, acc: number = 1): Promise> => { + await sleep(10); + return n + ? factorial.cont(n - 1, acc * n) + : acc; + }); + + expect(await factorial(2)).toBe(2); + }); }); });