Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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