Skip to content

Commit

Permalink
feat(function): add function monoid (id, compose, pipe) (#13)
Browse files Browse the repository at this point in the history
* feat(function): add id function

* feat(function): add compose function

* feat(function): add pipe function
  • Loading branch information
Ailrun committed Aug 18, 2018
1 parent 4af7d3a commit 76618ae
Show file tree
Hide file tree
Showing 7 changed files with 378 additions and 0 deletions.
124 changes: 124 additions & 0 deletions packages/function/src/compose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2018-present Junyoung Clare Jang
*/
import { id } from './';

export function compose(): typeof id;
export function compose<A0 extends any[], A1>(
f0: (...args: A0) => A1,
): (...args: A0) => A1;
export function compose<A0 extends any[], A1, A2>(
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A2;
export function compose<A0 extends any[], A1, A2, A3>(
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A3;
export function compose<A0 extends any[], A1, A2, A3, A4>(
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A4;
export function compose<A0 extends any[], A1, A2, A3, A4, A5>(
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A5;
export function compose<A0 extends any[], A1, A2, A3, A4, A5, A6>(
f5: (a5: A5) => A6,
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A6;
export function compose<A0 extends any[], A1, A2, A3, A4, A5, A6, A7>(
f6: (a6: A6) => A7,
f5: (a5: A5) => A6,
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A7;
export function compose<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8>(
f7: (a7: A7) => A8,
f6: (a6: A6) => A7,
f5: (a5: A5) => A6,
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A8;
export function compose<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8, A9>(
f8: (a8: A8) => A9,
f7: (a7: A7) => A8,
f6: (a6: A6) => A7,
f5: (a5: A5) => A6,
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A9;
//tslint:disable-next-line: max-line-length
export function compose<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8, A9, A10>(
f9: (a9: A9) => A10,
f8: (a8: A8) => A9,
f7: (a7: A7) => A8,
f6: (a6: A6) => A7,
f5: (a5: A5) => A6,
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A10;
//tslint:disable-next-line: max-line-length
export function compose<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11>(
f10: (a10: A10) => A11,
f9: (a9: A9) => A10,
f8: (a8: A8) => A9,
f7: (a7: A7) => A8,
f6: (a6: A6) => A7,
f5: (a5: A5) => A6,
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A11;
//tslint:disable-next-line: max-line-length
export function compose<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12>(
f11: (a10: A11) => A12,
f10: (a10: A10) => A11,
f9: (a9: A9) => A10,
f8: (a8: A8) => A9,
f7: (a7: A7) => A8,
f6: (a6: A6) => A7,
f5: (a5: A5) => A6,
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f0: (...args: A0) => A1,
): (...args: A0) => A12;
export function compose(
...fns: ((...args: any[]) => any)[]
): (...args: any[]) => any {
if (fns.length === 0) {
return id;
}

return function (...args) {
return fns.reduceRight((acc, fn) => {
return [fn(...acc)];
}, args)[0];
};
}
6 changes: 6 additions & 0 deletions packages/function/src/id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright 2018-present Junyoung Clare Jang
*/
export function id<T>(t: T): T {
return t;
}
3 changes: 3 additions & 0 deletions packages/function/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
* Copyright 2018-present Junyoung Clare Jang
*/
export * from './types';
export * from './id';
export * from './compose';
export * from './pipe';
export * from './curry';
124 changes: 124 additions & 0 deletions packages/function/src/pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright 2018-present Junyoung Clare Jang
*/
import { id } from './';

export function pipe(): typeof id;
export function pipe<A0 extends any[], A1>(
f0: (...args: A0) => A1,
): (...args: A0) => A1;
export function pipe<A0 extends any[], A1, A2>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
): (...args: A0) => A2;
export function pipe<A0 extends any[], A1, A2, A3>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f2: (a2: A2) => A3,
): (...args: A0) => A3;
export function pipe<A0 extends any[], A1, A2, A3, A4>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f2: (a2: A2) => A3,
f3: (a3: A3) => A4,
): (...args: A0) => A4;
export function pipe<A0 extends any[], A1, A2, A3, A4, A5>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f2: (a2: A2) => A3,
f3: (a3: A3) => A4,
f4: (a4: A4) => A5,
): (...args: A0) => A5;
export function pipe<A0 extends any[], A1, A2, A3, A4, A5, A6>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f2: (a2: A2) => A3,
f3: (a3: A3) => A4,
f4: (a4: A4) => A5,
f5: (a5: A5) => A6,
): (...args: A0) => A6;
export function pipe<A0 extends any[], A1, A2, A3, A4, A5, A6, A7>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f2: (a2: A2) => A3,
f3: (a3: A3) => A4,
f4: (a4: A4) => A5,
f5: (a5: A5) => A6,
f6: (a6: A6) => A7,
): (...args: A0) => A7;
export function pipe<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f2: (a2: A2) => A3,
f3: (a3: A3) => A4,
f4: (a4: A4) => A5,
f5: (a5: A5) => A6,
f6: (a6: A6) => A7,
f7: (a7: A7) => A8,
): (...args: A0) => A8;
export function pipe<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8, A9>(
f0: (...args: A0) => A1,
f4: (a4: A4) => A5,
f3: (a3: A3) => A4,
f2: (a2: A2) => A3,
f1: (a1: A1) => A2,
f5: (a5: A5) => A6,
f6: (a6: A6) => A7,
f7: (a7: A7) => A8,
f8: (a8: A8) => A9,
): (...args: A0) => A9;
//tslint:disable-next-line: max-line-length
export function pipe<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8, A9, A10>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f2: (a2: A2) => A3,
f3: (a3: A3) => A4,
f4: (a4: A4) => A5,
f5: (a5: A5) => A6,
f6: (a6: A6) => A7,
f7: (a7: A7) => A8,
f8: (a8: A8) => A9,
f9: (a9: A9) => A10,
): (...args: A0) => A10;
//tslint:disable-next-line: max-line-length
export function pipe<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f2: (a2: A2) => A3,
f3: (a3: A3) => A4,
f4: (a4: A4) => A5,
f5: (a5: A5) => A6,
f6: (a6: A6) => A7,
f7: (a7: A7) => A8,
f8: (a8: A8) => A9,
f9: (a9: A9) => A10,
f10: (a10: A10) => A11,
): (...args: A0) => A11;
//tslint:disable-next-line: max-line-length
export function pipe<A0 extends any[], A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12>(
f0: (...args: A0) => A1,
f1: (a1: A1) => A2,
f6: (a6: A6) => A7,
f2: (a2: A2) => A3,
f3: (a3: A3) => A4,
f4: (a4: A4) => A5,
f5: (a5: A5) => A6,
f7: (a7: A7) => A8,
f8: (a8: A8) => A9,
f9: (a9: A9) => A10,
f10: (a10: A10) => A11,
f11: (a10: A11) => A12,
): (...args: A0) => A12;
export function pipe(
...fns: ((...args: any[]) => any)[]
): (...args: any[]) => any {
if (fns.length === 0) {
return id;
}

return function (...args) {
return fns.reduce((acc, fn) => {
return [fn(...acc)];
}, args)[0];
};
}
55 changes: 55 additions & 0 deletions packages/function/tests/compose.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Fun } from '@typed-f/function';
import { compose } from '@typed-f/function/dist/compose';

describe('compose', () => {
it('should return a function that runs each function once from right to left', () => {
const fn1: Fun<[number], number> = jest.fn((x: number) => x + 1);
const fn2: Fun<[number], number[]> = jest.fn((size: number) => new Array(size).fill(0));
compose(fn2, fn1)(0);
expect(fn1).toHaveBeenCalledTimes(1);
expect(fn2).toHaveBeenCalledTimes(1);
});

it('should return a function that returns the result of finally executed function', () => {
const fn1: Fun<[string], string> = jest.fn((str: string) => str.toUpperCase());
const fn2: Fun<[string], string[]> = jest.fn((str: string) => str.split(''));
const fn3: Fun<[string[]], number[]> = jest.fn((arr: string[]) => arr.map((str) => str.charCodeAt(0)));
const fn4: Fun<[number[]], number[]> = jest.fn((arr: number[]) => arr.map((code) => code + 1));
const fn5: Fun<[number[]], string> = jest.fn((arr: number[]) => String.fromCharCode(...arr));

const fnComposed = compose(fn5, fn4, fn3, fn2, fn1);

expect(fn5).toHaveLastReturnedWith(fnComposed('abc'));
});

it('should return a function that runs next function with the result of previous funciton', () => {
const fn1: Fun<[number], string> = jest.fn((x: number) => x.toString());
const fn2: Fun<[string], RegExp> = jest.fn((str: string) => new RegExp(str));
const fn3: Fun<[RegExp], Fun<[string], boolean>> = jest.fn((regexp: RegExp) => (x: string) => regexp.test(x));

compose(fn3, fn2, fn1)(5210312);

expect(fn2).toHaveBeenLastCalledWith((fn1 as jest.Mock).mock.results[0].value);
expect(fn3).toHaveBeenLastCalledWith((fn2 as jest.Mock).mock.results[0].value);
});

it('should return an function that does not call next functions if some function throws error', () => {
const fn1: Fun<[number], number> = jest.fn((x: number) => x * x);
const fn2: Fun<[number], number> = jest.fn((x: number) => {
if (x >= 0) {
throw new Error('Input should be smaller than 0');
}

return Math.sqrt(-x);
});
const fn3: Fun<[number], number> = jest.fn((x: number) => x + 1);
const fn4: Fun<[number], string> = jest.fn((x: number) => x.toFixed(2));

expect(() => {
compose(fn4, fn3, fn2, fn1)(1);
}).toThrowError();
expect(fn1).toHaveBeenCalledTimes(1);
expect(fn2).toHaveBeenCalledTimes(1);
expect(fn3).toHaveBeenCalledTimes(0);
});
});
11 changes: 11 additions & 0 deletions packages/function/tests/id.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { id } from '@typed-f/function/dist/id';

describe('id', () => {
it('should return its argument itself', () => {
expect(id(5)).toEqual(5);
expect(id(undefined)).toEqual(undefined);
expect(id({ x: 4 })).toEqual({ x: 4 });
expect(id([1, 2, 'ab'])).toEqual([1, 2, 'ab']);
expect(id(/abc/)).toEqual(/abc/);
});
});
55 changes: 55 additions & 0 deletions packages/function/tests/pipe.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Fun } from '@typed-f/function';
import { pipe } from '@typed-f/function/dist/pipe';

describe('pipe', () => {
it('should return a function that runs each function once from left to right', () => {
const fn1: Fun<[number], number> = jest.fn((x: number) => x + 1);
const fn2: Fun<[number], number[]> = jest.fn((size: number) => new Array(size).fill(0));
pipe(fn1, fn2)(0);
expect(fn1).toHaveBeenCalledTimes(1);
expect(fn2).toHaveBeenCalledTimes(1);
});

it('should return a function that returns the result of finally executed function', () => {
const fn1: Fun<[string], string> = jest.fn((str: string) => str.toUpperCase());
const fn2: Fun<[string], string[]> = jest.fn((str: string) => str.split(''));
const fn3: Fun<[string[]], number[]> = jest.fn((arr: string[]) => arr.map((str) => str.charCodeAt(0)));
const fn4: Fun<[number[]], number[]> = jest.fn((arr: number[]) => arr.map((code) => code + 1));
const fn5: Fun<[number[]], string> = jest.fn((arr: number[]) => String.fromCharCode(...arr));

const fnComposed = pipe(fn1, fn2, fn3, fn4, fn5);

expect(fn5).toHaveLastReturnedWith(fnComposed('abc'));
});

it('should return a function that runs next function with the result of previous funciton', () => {
const fn1: Fun<[number], string> = jest.fn((x: number) => x.toString());
const fn2: Fun<[string], RegExp> = jest.fn((str: string) => new RegExp(str));
const fn3: Fun<[RegExp], Fun<[string], boolean>> = jest.fn((regexp: RegExp) => (x: string) => regexp.test(x));

pipe(fn1, fn2, fn3)(5210312);

expect(fn2).toHaveBeenLastCalledWith((fn1 as jest.Mock).mock.results[0].value);
expect(fn3).toHaveBeenLastCalledWith((fn2 as jest.Mock).mock.results[0].value);
});

it('should return an function that does not call next functions if some function throws error', () => {
const fn1: Fun<[number], number> = jest.fn((x: number) => x * x);
const fn2: Fun<[number], number> = jest.fn((x: number) => {
if (x >= 0) {
throw new Error('Input should be smaller than 0');
}

return Math.sqrt(-x);
});
const fn3: Fun<[number], number> = jest.fn((x: number) => x + 1);
const fn4: Fun<[number], string> = jest.fn((x: number) => x.toFixed(2));

expect(() => {
pipe(fn1, fn2, fn3, fn4)(1);
}).toThrowError();
expect(fn1).toHaveBeenCalledTimes(1);
expect(fn2).toHaveBeenCalledTimes(1);
expect(fn3).toHaveBeenCalledTimes(0);
});
});

0 comments on commit 76618ae

Please sign in to comment.