From 4bb2659fba42b1a70ef25a9ae60933939382f5ad Mon Sep 17 00:00:00 2001 From: Utopia Date: Wed, 22 Feb 2023 22:26:16 +0800 Subject: [PATCH] feat: new Function - compose --- packages/core/src/compose.test.ts | 56 +++++++++++++++++++++++++++++++ packages/core/src/compose.ts | 33 ++++++++++++++++++ packages/core/src/index.ts | 1 + 3 files changed, 90 insertions(+) create mode 100644 packages/core/src/compose.test.ts create mode 100644 packages/core/src/compose.ts diff --git a/packages/core/src/compose.test.ts b/packages/core/src/compose.test.ts new file mode 100644 index 0000000..c80a427 --- /dev/null +++ b/packages/core/src/compose.test.ts @@ -0,0 +1,56 @@ +import { compose } from './compose' + +describe('compose', () => { + it('composes from right to left', () => { + const double = (x: number) => x * 2 + const square = (x: number) => x * x + expect(compose(square)(5)).toBe(25) + expect(compose(square, double)(5)).toBe(100) + expect(compose(double, square, double)(5)).toBe(200) + }) + + it('composes functions from right to left', () => { + const a = (next: (x: string) => string) => (x: string) => next(`${x}a`) + const b = (next: (x: string) => string) => (x: string) => next(`${x}b`) + const c = (next: (x: string) => string) => (x: string) => next(`${x}c`) + const final = (x: string) => x + + expect(compose(a, b, c)(final)('')).toBe('abc') + expect(compose(b, c, a)(final)('')).toBe('bca') + expect(compose(c, a, b)(final)('')).toBe('cab') + }) + + it('throws at runtime if argument is not a function', () => { + type sFunc = (x: number, y: number) => number + const square = (x: number) => x * x + + expect( + () => compose(square, false as unknown as sFunc)(1, 2)) + .toThrow() + expect( + // @ts-expect-error for test + () => compose(square, undefined)(1, 2)) + .toThrow() + expect( + () => compose(square, true as unknown as sFunc)(1, 2)) + .toThrow() + expect( + () => compose(square, NaN as unknown as sFunc)(1, 2)) + .toThrow() + expect( + () => compose(square, '42' as unknown as sFunc)(1, 2)).toThrow() + }) + + it('can be seeded with multiple arguments', () => { + const square = (x: number) => x * x + const add = (x: number, y: number) => x + y + expect(compose(square, add)(1, 2)).toBe(9) + }) + + it('returns the given arguments if given no functions', () => { + // @ts-expect-error for test + expect(compose()(1, 2)).toEqual([1, 2]) + // @ts-expect-error for test + expect(compose()(3)).toEqual([3]) + }) +}) diff --git a/packages/core/src/compose.ts b/packages/core/src/compose.ts new file mode 100644 index 0000000..aa4fff5 --- /dev/null +++ b/packages/core/src/compose.ts @@ -0,0 +1,33 @@ +type Fn = (args: Arg) => V +type Fn2 = (...args: Args) => V + +/** + * Composes single-argument functions from right to left. The rightmost + * function can take multiple arguments as it provides the signature for the + * resulting composite function. + * @param {Function[]} fns The functions to compose. + * @returns A function obtained by composing the argument functions from right + * to left. For example, `compose(f, g, h)` is identical to doing + * `(...args) => f(g(h(...args)))`. + */ +export function compose(f1: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn, f5: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn, f5: Fn, f6: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn, f5: Fn, f6: Fn, f7: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn, f5: Fn, f6: Fn, f7: Fn, f8: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn, f5: Fn, f6: Fn, f7: Fn, f8: Fn, f9: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn, f5: Fn, f6: Fn, f7: Fn, f8: Fn, f9: Fn, f10: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn, f5: Fn, f6: Fn, f7: Fn, f8: Fn, f9: Fn, f10: Fn, f11: Fn2): (...args: Args) => R1 +export function compose(f1: Fn, f2: Fn, f3: Fn, f4: Fn, f5: Fn, f6: Fn, f7: Fn, f8: Fn, f9: Fn, f10: Fn, f11: Fn, f12: Fn2): (...args: Args) => R1 +export function compose(...fns: Function[]) { + if (fns.length === 0) + return (...args: T) => args + + const fn = fns.pop()! + return function (this: any, ...args: any[]) { + return fns.reduceRight((acc, cur) => cur.call(this, acc), fn(...args)) + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1e2196c..8d74a03 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,6 +5,7 @@ export * from '@utopia-utils/share' export * from './awaitTo' export * from './callLimit' export * from './capitalize' +export * from './compose' export * from './createEnumFromOptions' export * from './csv' export * from './deepClone'