Skip to content

Commit

Permalink
Merge pull request #2 from helabenkhalfallah/feature/functional-patterns
Browse files Browse the repository at this point in the history
[FEATURE]: add functional programming patterns
  • Loading branch information
helabenkhalfallah authored Feb 14, 2025
2 parents 42ece09 + b646e4b commit b834ec5
Show file tree
Hide file tree
Showing 31 changed files with 2,394 additions and 58 deletions.
143 changes: 90 additions & 53 deletions README.md

Large diffs are not rendered by default.

155 changes: 155 additions & 0 deletions benchmark-fp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { performance } from 'perf_hooks';

import {
CanApply,
Effect,
Ok,
Option,
compose,
curry,
partial,
partialRight,
pipe,
uncurry,
} from './src/index.ts';

type BenchmarkResult = {
function: string;
operation: string;
time: number;
};

function benchmark<T>(fn: () => T, label: string, operation: string): BenchmarkResult {
const start = performance.now();
fn();
const time = performance.now() - start;
return { function: label, operation, time };
}

const benchmarks: BenchmarkResult[] = [];

// Function Composition
benchmarks.push(
benchmark(
() => {
const add = (a: number) => a + 2;
const multiply = (a: number) => a * 3;
const composed = compose(multiply, add);
composed(5);
},
'compose',
'Function composition',
),
);

benchmarks.push(
benchmark(
() => {
const add = (a: number) => a + 2;
const multiply = (a: number) => a * 3;
const piped = pipe(add, multiply);
piped(5);
},
'pipe',
'Function piping',
),
);

// Currying & Partial Application
benchmarks.push(
benchmark(
() => {
const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
curriedAdd(3)(5);
},
'curry',
'Currying',
),
);

benchmarks.push(
benchmark(
() => {
const subtract = (a: number, b: number) => a - b;
const partiallyApplied = partial(subtract, 10);
partiallyApplied(3);
},
'partial',
'Partial Application',
),
);

benchmarks.push(
benchmark(
() => {
const subtract = (a: number, b: number) => a - b;
const partiallyAppliedRight = partialRight(subtract, 3);
partiallyAppliedRight(10);
},
'partialRight',
'Partial Right Application',
),
);

benchmarks.push(
benchmark(
() => {
const add = (a: number, b: number) => a + b;
const curriedAdd = curry(add);
const uncurriedAdd = uncurry(curriedAdd);
uncurriedAdd(3, 5);
},
'uncurry',
'Uncurrying',
),
);

// Functors: CanApply
benchmarks.push(
benchmark(
() => {
CanApply(5)
.map((x) => x * 2)
.map((x) => x + 10)
.getValue();
},
'CanApply',
'Functor Mapping',
),
);

// Monads: Option, Result, and Effect
benchmarks.push(
benchmark(
() => {
Option.from(5)
.map((x) => x * 2)
.getOrElse(0);
},
'Option',
'Option Mapping',
),
);

benchmarks.push(
benchmark(
() => {
new Ok(5).map((x) => x * 2).unwrapOr(0);
},
'Result',
'Result Mapping',
),
);

benchmarks.push(
benchmark(
() => {
Effect(() => 5 * 2).run();
},
'Effect',
'Effect Execution',
),
);

console.table(benchmarks);
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dsa-toolbox",
"version": "1.0.2",
"version": "2.0.0",
"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.",
"type": "module",
"main": "dist/index.js",
Expand All @@ -13,6 +13,7 @@
"update-docs": "rimraf ./docs && typedoc",
"build": "rimraf ./dist && tsc",
"bench-ds": "nodemon --exec node --loader ts-node/esm ./benchmark-ds.ts -- --dev",
"bench-fp": "nodemon --exec node --loader ts-node/esm ./benchmark-fp.ts -- --dev",
"bench-algo": "nodemon --exec node --loader ts-node/esm ./benchmark-algo.ts -- --dev",
"lint": "eslint --config ./eslint.config.mjs --cache-location ./.eslintcache \"./**/*.ts\" --cache",
"lint:fix": "eslint --config ./eslint.config.mjs --cache-location ./.eslintcache \"./**/*.ts\" --cache --fix",
Expand All @@ -36,6 +37,7 @@
"eslint": "=9.0.0",
"eslint-config-prettier": "=9.1.0",
"eslint-plugin-prettier": "=5.2.1",
"fast-check": "=3.23.2",
"husky": "=9.1.6",
"nodemon": "=2.0.19",
"rimraf": "=6.0.1",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/data-structures/trees/bst/BinarySearchTree-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { BinarySearchTree } from './BinarySearchTree.js';
import { BinarySearchTree } from './BinarySearchTree.ts';

describe('BinarySearchTree', () => {
it('should create an empty tree', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/data-structures/trees/red-black/RedBlackTree-test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';

import { ComparableNode } from '../../../commons/ComparableNode.js';
import { Color, RedBlackTree } from './RedBlackTree.js';
import { ComparableNode } from '../../../commons/ComparableNode.ts';
import { Color, RedBlackTree } from './RedBlackTree.ts';

describe('RedBlackTree', () => {
it('should create an empty Red-Black Tree', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/data-structures/trees/trie/Trie-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';

import { Trie } from './Trie.js';
import { Trie } from './Trie.ts';

describe('Trie', () => {
it('should insert and search for a word', () => {
Expand Down
103 changes: 103 additions & 0 deletions src/functional/composition/Composition-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { describe, expect, it } from 'vitest';

import { compose, pipe } from './Composition.ts';

describe('Functional Composition', () => {
const trim = (s: string): string => s.trim();
const toUpperCase = (s: string): string => s.toUpperCase();
const exclaim = (s: string): string => `${s}!`;

it('should compose functions from right to left', () => {
const composedFn = compose(exclaim, toUpperCase, trim);
expect(composedFn(' hello ')).toBe('HELLO!');
});

it('should pipe functions from left to right', () => {
const pipedFn = pipe(trim, toUpperCase, exclaim);
expect(pipedFn(' hello ')).toBe('HELLO!');
});

it('should return the same value when no functions are passed', () => {
expect(compose()(5)).toBe(5);
expect(pipe()(5)).toBe(5);
});
});

describe('Edge Cases', () => {
it('should return the same value when no functions are passed', () => {
expect(compose()(5)).toBe(5);
expect(pipe()(5)).toBe(5);
});

it('should handle single function cases correctly', () => {
const identity = (x: number) => x;
expect(compose(identity)(5)).toBe(5);
expect(pipe(identity)(5)).toBe(5);
});

it('should not mutate input', () => {
const obj = { value: 'hello' };

// Pure function that creates a new object
const cloneAndModify = (o: { value: string }) => ({ ...o, modified: true });

const result = compose(cloneAndModify)(obj);

expect(result).toEqual({ value: 'hello', modified: true }); // Same values
expect(result).not.toBe(obj); // Different object (immutability check)
});

it('should handle large number of functions', () => {
const functions = Array(1000).fill((x: number) => x + 1);
expect(compose(...functions)(0)).toBe(1000);
expect(pipe(...functions)(0)).toBe(1000);
});
});

describe('Purity Tests', () => {
it('should not have side effects', () => {
const sideEffect = 0;

// Pure function (does not modify external state)
const pureFunction = (x: number) => x + 1;

// Ensure pure functions do not modify external state
const composedPure = compose(pureFunction, pureFunction);

// Track sideEffect before execution
const before = sideEffect;

// Execute composed function
composedPure(5);

// Track sideEffect after execution
const after = sideEffect;

// Ensure external state is unchanged
expect(after).toBe(before);
});

it('should detect impure functions', () => {
let sideEffect = 0;

// Impure function (modifies external state)
const impureFunction = (x: number) => {
sideEffect += 1; // Side effect occurs
return x + 1;
};

const pureFunction = (x: number) => x + 1;

// Track sideEffect before execution
const before = sideEffect;

// Run an impure function inside compose
compose(pureFunction, impureFunction)(5);

// Track sideEffect after execution
const after = sideEffect;

// Side effect must have changed (which means impurity was detected)
expect(after).not.toBe(before);
});
});
40 changes: 40 additions & 0 deletions src/functional/composition/Composition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Composes multiple functions from right to left.
*
* @template T - The type of input and output for all functions.
* @param {...Array<(arg: T) => T>} fns - The functions to compose.
* @returns {(arg: T) => T} - A function that applies the composed functions from right to left.
*
* @example
* const trim = (s: string): string => s.trim();
* const toUpperCase = (s: string): string => s.toUpperCase();
* const exclaim = (s: string): string => `${s}!`;
*
* const composedFn = compose(exclaim, toUpperCase, trim);
* console.log(composedFn(" hello ")); // "HELLO!"
*/
export function compose<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
if (fns.some((fn) => typeof fn !== 'function')) {
throw new Error('All arguments to compose must be functions.');
}
return (arg: T) => fns.reduceRight((acc, fn) => fn(acc), arg);
}

/**
* Pipes multiple functions from left to right.
*
* @template T - The type of input and output for all functions.
* @param {...Array<(arg: T) => T>} fns - The functions to apply sequentially.
* @returns {(arg: T) => T} - A function that applies the piped functions from left to right.
*
* @example
* const trim = (s: string): string => s.trim();
* const toUpperCase = (s: string): string => s.toUpperCase();
* const exclaim = (s: string): string => `${s}!`;
*
* const pipedFn = pipe(trim, toUpperCase, exclaim);
* console.log(pipedFn(" hello ")); // "HELLO!"
*/
export function pipe<T>(...fns: Array<(arg: T) => T>): (arg: T) => T {
return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg);
}
Loading

0 comments on commit b834ec5

Please sign in to comment.