Skip to content

Commit b646e4b

Browse files
[FEATURE]: add functional programming patterns
1 parent 42ece09 commit b646e4b

31 files changed

+2394
-58
lines changed

README.md

Lines changed: 90 additions & 53 deletions
Large diffs are not rendered by default.

benchmark-fp.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { performance } from 'perf_hooks';
2+
3+
import {
4+
CanApply,
5+
Effect,
6+
Ok,
7+
Option,
8+
compose,
9+
curry,
10+
partial,
11+
partialRight,
12+
pipe,
13+
uncurry,
14+
} from './src/index.ts';
15+
16+
type BenchmarkResult = {
17+
function: string;
18+
operation: string;
19+
time: number;
20+
};
21+
22+
function benchmark<T>(fn: () => T, label: string, operation: string): BenchmarkResult {
23+
const start = performance.now();
24+
fn();
25+
const time = performance.now() - start;
26+
return { function: label, operation, time };
27+
}
28+
29+
const benchmarks: BenchmarkResult[] = [];
30+
31+
// Function Composition
32+
benchmarks.push(
33+
benchmark(
34+
() => {
35+
const add = (a: number) => a + 2;
36+
const multiply = (a: number) => a * 3;
37+
const composed = compose(multiply, add);
38+
composed(5);
39+
},
40+
'compose',
41+
'Function composition',
42+
),
43+
);
44+
45+
benchmarks.push(
46+
benchmark(
47+
() => {
48+
const add = (a: number) => a + 2;
49+
const multiply = (a: number) => a * 3;
50+
const piped = pipe(add, multiply);
51+
piped(5);
52+
},
53+
'pipe',
54+
'Function piping',
55+
),
56+
);
57+
58+
// Currying & Partial Application
59+
benchmarks.push(
60+
benchmark(
61+
() => {
62+
const add = (a: number, b: number) => a + b;
63+
const curriedAdd = curry(add);
64+
curriedAdd(3)(5);
65+
},
66+
'curry',
67+
'Currying',
68+
),
69+
);
70+
71+
benchmarks.push(
72+
benchmark(
73+
() => {
74+
const subtract = (a: number, b: number) => a - b;
75+
const partiallyApplied = partial(subtract, 10);
76+
partiallyApplied(3);
77+
},
78+
'partial',
79+
'Partial Application',
80+
),
81+
);
82+
83+
benchmarks.push(
84+
benchmark(
85+
() => {
86+
const subtract = (a: number, b: number) => a - b;
87+
const partiallyAppliedRight = partialRight(subtract, 3);
88+
partiallyAppliedRight(10);
89+
},
90+
'partialRight',
91+
'Partial Right Application',
92+
),
93+
);
94+
95+
benchmarks.push(
96+
benchmark(
97+
() => {
98+
const add = (a: number, b: number) => a + b;
99+
const curriedAdd = curry(add);
100+
const uncurriedAdd = uncurry(curriedAdd);
101+
uncurriedAdd(3, 5);
102+
},
103+
'uncurry',
104+
'Uncurrying',
105+
),
106+
);
107+
108+
// Functors: CanApply
109+
benchmarks.push(
110+
benchmark(
111+
() => {
112+
CanApply(5)
113+
.map((x) => x * 2)
114+
.map((x) => x + 10)
115+
.getValue();
116+
},
117+
'CanApply',
118+
'Functor Mapping',
119+
),
120+
);
121+
122+
// Monads: Option, Result, and Effect
123+
benchmarks.push(
124+
benchmark(
125+
() => {
126+
Option.from(5)
127+
.map((x) => x * 2)
128+
.getOrElse(0);
129+
},
130+
'Option',
131+
'Option Mapping',
132+
),
133+
);
134+
135+
benchmarks.push(
136+
benchmark(
137+
() => {
138+
new Ok(5).map((x) => x * 2).unwrapOr(0);
139+
},
140+
'Result',
141+
'Result Mapping',
142+
),
143+
);
144+
145+
benchmarks.push(
146+
benchmark(
147+
() => {
148+
Effect(() => 5 * 2).run();
149+
},
150+
'Effect',
151+
'Effect Execution',
152+
),
153+
);
154+
155+
console.table(benchmarks);

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dsa-toolbox",
3-
"version": "1.0.2",
3+
"version": "2.0.0",
44
"description": "A powerful toolkit for data structures and algorithms in TypeScript, designed for optimal performance and versatility. The toolkit provides implementations of various data structures and algorithms, with a focus on search and sort operations, caching, and probabilistic data structures.",
55
"type": "module",
66
"main": "dist/index.js",
@@ -13,6 +13,7 @@
1313
"update-docs": "rimraf ./docs && typedoc",
1414
"build": "rimraf ./dist && tsc",
1515
"bench-ds": "nodemon --exec node --loader ts-node/esm ./benchmark-ds.ts -- --dev",
16+
"bench-fp": "nodemon --exec node --loader ts-node/esm ./benchmark-fp.ts -- --dev",
1617
"bench-algo": "nodemon --exec node --loader ts-node/esm ./benchmark-algo.ts -- --dev",
1718
"lint": "eslint --config ./eslint.config.mjs --cache-location ./.eslintcache \"./**/*.ts\" --cache",
1819
"lint:fix": "eslint --config ./eslint.config.mjs --cache-location ./.eslintcache \"./**/*.ts\" --cache --fix",
@@ -36,6 +37,7 @@
3637
"eslint": "=9.0.0",
3738
"eslint-config-prettier": "=9.1.0",
3839
"eslint-plugin-prettier": "=5.2.1",
40+
"fast-check": "=3.23.2",
3941
"husky": "=9.1.6",
4042
"nodemon": "=2.0.19",
4143
"rimraf": "=6.0.1",

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/data-structures/trees/bst/BinarySearchTree-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { BinarySearchTree } from './BinarySearchTree.js';
3+
import { BinarySearchTree } from './BinarySearchTree.ts';
44

55
describe('BinarySearchTree', () => {
66
it('should create an empty tree', () => {

src/data-structures/trees/red-black/RedBlackTree-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { ComparableNode } from '../../../commons/ComparableNode.js';
4-
import { Color, RedBlackTree } from './RedBlackTree.js';
3+
import { ComparableNode } from '../../../commons/ComparableNode.ts';
4+
import { Color, RedBlackTree } from './RedBlackTree.ts';
55

66
describe('RedBlackTree', () => {
77
it('should create an empty Red-Black Tree', () => {

src/data-structures/trees/trie/Trie-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { Trie } from './Trie.js';
3+
import { Trie } from './Trie.ts';
44

55
describe('Trie', () => {
66
it('should insert and search for a word', () => {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import { compose, pipe } from './Composition.ts';
4+
5+
describe('Functional Composition', () => {
6+
const trim = (s: string): string => s.trim();
7+
const toUpperCase = (s: string): string => s.toUpperCase();
8+
const exclaim = (s: string): string => `${s}!`;
9+
10+
it('should compose functions from right to left', () => {
11+
const composedFn = compose(exclaim, toUpperCase, trim);
12+
expect(composedFn(' hello ')).toBe('HELLO!');
13+
});
14+
15+
it('should pipe functions from left to right', () => {
16+
const pipedFn = pipe(trim, toUpperCase, exclaim);
17+
expect(pipedFn(' hello ')).toBe('HELLO!');
18+
});
19+
20+
it('should return the same value when no functions are passed', () => {
21+
expect(compose()(5)).toBe(5);
22+
expect(pipe()(5)).toBe(5);
23+
});
24+
});
25+
26+
describe('Edge Cases', () => {
27+
it('should return the same value when no functions are passed', () => {
28+
expect(compose()(5)).toBe(5);
29+
expect(pipe()(5)).toBe(5);
30+
});
31+
32+
it('should handle single function cases correctly', () => {
33+
const identity = (x: number) => x;
34+
expect(compose(identity)(5)).toBe(5);
35+
expect(pipe(identity)(5)).toBe(5);
36+
});
37+
38+
it('should not mutate input', () => {
39+
const obj = { value: 'hello' };
40+
41+
// Pure function that creates a new object
42+
const cloneAndModify = (o: { value: string }) => ({ ...o, modified: true });
43+
44+
const result = compose(cloneAndModify)(obj);
45+
46+
expect(result).toEqual({ value: 'hello', modified: true }); // Same values
47+
expect(result).not.toBe(obj); // Different object (immutability check)
48+
});
49+
50+
it('should handle large number of functions', () => {
51+
const functions = Array(1000).fill((x: number) => x + 1);
52+
expect(compose(...functions)(0)).toBe(1000);
53+
expect(pipe(...functions)(0)).toBe(1000);
54+
});
55+
});
56+
57+
describe('Purity Tests', () => {
58+
it('should not have side effects', () => {
59+
const sideEffect = 0;
60+
61+
// Pure function (does not modify external state)
62+
const pureFunction = (x: number) => x + 1;
63+
64+
// Ensure pure functions do not modify external state
65+
const composedPure = compose(pureFunction, pureFunction);
66+
67+
// Track sideEffect before execution
68+
const before = sideEffect;
69+
70+
// Execute composed function
71+
composedPure(5);
72+
73+
// Track sideEffect after execution
74+
const after = sideEffect;
75+
76+
// Ensure external state is unchanged
77+
expect(after).toBe(before);
78+
});
79+
80+
it('should detect impure functions', () => {
81+
let sideEffect = 0;
82+
83+
// Impure function (modifies external state)
84+
const impureFunction = (x: number) => {
85+
sideEffect += 1; // Side effect occurs
86+
return x + 1;
87+
};
88+
89+
const pureFunction = (x: number) => x + 1;
90+
91+
// Track sideEffect before execution
92+
const before = sideEffect;
93+
94+
// Run an impure function inside compose
95+
compose(pureFunction, impureFunction)(5);
96+
97+
// Track sideEffect after execution
98+
const after = sideEffect;
99+
100+
// Side effect must have changed (which means impurity was detected)
101+
expect(after).not.toBe(before);
102+
});
103+
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Composes multiple functions from right to left.
3+
*
4+
* @template T - The type of input and output for all functions.
5+
* @param {...Array<(arg: T) => T>} fns - The functions to compose.
6+
* @returns {(arg: T) => T} - A function that applies the composed functions from right to left.
7+
*
8+
* @example
9+
* const trim = (s: string): string => s.trim();
10+
* const toUpperCase = (s: string): string => s.toUpperCase();
11+
* const exclaim = (s: string): string => `${s}!`;
12+
*
13+
* const composedFn = compose(exclaim, toUpperCase, trim);
14+
* console.log(composedFn(" hello ")); // "HELLO!"
15+
*/
16+
export function compose<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
17+
if (fns.some((fn) => typeof fn !== 'function')) {
18+
throw new Error('All arguments to compose must be functions.');
19+
}
20+
return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);
21+
}
22+
23+
/**
24+
* Pipes multiple functions from left to right.
25+
*
26+
* @template T - The type of input and output for all functions.
27+
* @param {...Array<(arg: T) => T>} fns - The functions to apply sequentially.
28+
* @returns {(arg: T) => T} - A function that applies the piped functions from left to right.
29+
*
30+
* @example
31+
* const trim = (s: string): string => s.trim();
32+
* const toUpperCase = (s: string): string => s.toUpperCase();
33+
* const exclaim = (s: string): string => `${s}!`;
34+
*
35+
* const pipedFn = pipe(trim, toUpperCase, exclaim);
36+
* console.log(pipedFn(" hello ")); // "HELLO!"
37+
*/
38+
export function pipe<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
39+
return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);
40+
}

0 commit comments

Comments
 (0)