Skip to content

Commit

Permalink
feat: new Function - compose
Browse files Browse the repository at this point in the history
  • Loading branch information
GreatAuk committed Feb 22, 2023
1 parent a26723a commit 4bb2659
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 0 deletions.
56 changes: 56 additions & 0 deletions packages/core/src/compose.test.ts
Original file line number Diff line number Diff line change
@@ -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])
})
})
33 changes: 33 additions & 0 deletions packages/core/src/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
type Fn<Arg, V> = (args: Arg) => V
type Fn2<Args extends unknown[], V> = (...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<Args extends unknown[], R1>(f1: Fn2<Args, R1>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2>(f1: Fn<R2, R1>, f2: Fn2<Args, R2>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn2<Args, R3>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn2<Args, R4>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4, R5>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn<R5, R4>, f5: Fn2<Args, R5>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4, R5, R6>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn<R5, R4>, f5: Fn<R6, R5>, f6: Fn2<Args, R6>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4, R5, R6, R7>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn<R5, R4>, f5: Fn<R6, R5>, f6: Fn<R7, R6>, f7: Fn2<Args, R7>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4, R5, R6, R7, R8>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn<R5, R4>, f5: Fn<R6, R5>, f6: Fn<R7, R6>, f7: Fn<R8, R7>, f8: Fn2<Args, R8>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4, R5, R6, R7, R8, R9>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn<R5, R4>, f5: Fn<R6, R5>, f6: Fn<R7, R6>, f7: Fn<R8, R7>, f8: Fn<R9, R8>, f9: Fn2<Args, R9>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4, R5, R6, R7, R8, R9, R10>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn<R5, R4>, f5: Fn<R6, R5>, f6: Fn<R7, R6>, f7: Fn<R8, R7>, f8: Fn<R9, R8>, f9: Fn<R10, R9>, f10: Fn2<Args, R10>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn<R5, R4>, f5: Fn<R6, R5>, f6: Fn<R7, R6>, f7: Fn<R8, R7>, f8: Fn<R9, R8>, f9: Fn<R10, R9>, f10: Fn<R11, R10>, f11: Fn2<Args, R11>): (...args: Args) => R1
export function compose<Args extends unknown[], R1, R2, R3, R4, R5, R6, R7, R8, R9, R10, R11, R12>(f1: Fn<R2, R1>, f2: Fn<R3, R2>, f3: Fn<R4, R3>, f4: Fn<R5, R4>, f5: Fn<R6, R5>, f6: Fn<R7, R6>, f7: Fn<R8, R7>, f8: Fn<R9, R8>, f9: Fn<R10, R9>, f10: Fn<R11, R10>, f11: Fn<R12, R11>, f12: Fn2<Args, R12>): (...args: Args) => R1
export function compose(...fns: Function[]) {
if (fns.length === 0)
return <T extends unknown[]>(...args: T) => args

const fn = fns.pop()!
return function (this: any, ...args: any[]) {
return fns.reduceRight((acc, cur) => cur.call(this, acc), fn(...args))
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit 4bb2659

Please sign in to comment.