UNPKG

trampoline-ts

Version:

A type-safe way to emulate tail-call optimization with trampolines

117 lines (93 loc) 4.05 kB
import { assert as typeAssert, IsExact, Has } from 'conditional-type-checks'; import { trampoline, isThunk, ThunkOrValue, trampolineAsync } from '../src'; import { ArgumentTypes } from '../src/types'; describe('trampoline', () => { describe('trampoline(fn)', () => { it('returns a tail call recursive function', () => { expect(trampoline(() => true)).toBeInstanceOf(Function); }); it('preserves argument types in the returned function', () => { const impl = (_1: number, _2: string, _3: boolean) => true; const fn = trampoline(impl); // tslint:disable-next-line:ban-types typeAssert<Has<typeof fn, Function>>(true); typeAssert<IsExact<ArgumentTypes<typeof fn>, [number, string, boolean]>>(true); }); it('preserves return type in the returned function', () => { const impl = () => true; const fn = trampoline(impl); // tslint:disable-next-line:ban-types typeAssert<Has<typeof fn, Function>>(true); typeAssert<IsExact<ReturnType<typeof fn>, boolean>>(true); }); it('removes "ThunkOrValue" from the returned functions return type', () => { const impl = (): ThunkOrValue<boolean> => true; const fn = trampoline(impl); // tslint:disable-next-line:ban-types typeAssert<Has<typeof fn, Function>>(true); typeAssert<IsExact<ReturnType<typeof fn>, boolean>>(true); }); it('preserves argument types in "cont"', () => { const impl = (_1: number, _2: string, _3: boolean) => true; const { cont } = trampoline(impl); // tslint:disable-next-line:ban-types typeAssert<Has<typeof cont, Function>>(true); typeAssert<IsExact<ArgumentTypes<typeof cont>, [number, string, boolean]>>(true); }); it('preserves return type in "cont" returned thunk', () => { const impl = () => true; const { cont } = trampoline(impl); const thunk = cont(); // tslint:disable-next-line:ban-types typeAssert<Has<typeof cont, Function>>(true); typeAssert<Has<ReturnType<typeof thunk>, boolean>>(true); }); it('returns a function with a thunk returning "cont" method', () => { const fn = jest.fn((input: string) => `${input}!`); const { cont } = trampoline(fn); const thunk = cont('input'); expect(isThunk(thunk)).toBe(true); expect(fn).not.toHaveBeenCalled(); expect(thunk()).toBe('input!'); expect(fn).toHaveBeenNthCalledWith(1, 'input'); }); it(`loops until the passed function doesn't return a thunk`, () => { const timesToLoop = 5; const fn = trampoline((times: number = 0): ThunkOrValue<number> => { return times < timesToLoop ? fn.cont(times + 1) : times; }); const contSpy = jest.spyOn(fn, 'cont'); fn(); expect(contSpy).toHaveBeenCalledTimes(5); }); it(`doesn't throw a stack overflow error`, () => { const brokenFactorial = (n: number, acc: number = 1): number => { return n ? brokenFactorial(n - 1, acc * n) : acc; }; const factorial = trampoline((n: number, acc: number = 1): ThunkOrValue<number> => { return n ? factorial.cont(n - 1, acc * n) : acc; }); expect(() => brokenFactorial(32768)).toThrowError('Maximum call stack size exceeded'); expect(factorial(32768)).toEqual(Infinity); }); it('supports returning functions', () => { const fn = trampoline((): ThunkOrValue<() => string> => { return () => 'hello'; }); 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); }); }); });