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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ Whether you're building a lightweight application or handling large datasets, **
- **Option<T>**: Safe handling of optional values (Some, None)
- **Result<T, E>**: Error handling without exceptions (Ok, Err)
- **Effect<T, E>**: Deferred computations with error safety
- **Pattern Matching**: Expressive control flow using Match (matcher, case-of)
- **Lenses & Optics**: Immutable state manipulation (Lens, Prism, Traversal)
- **Trampoline**: Converts deep recursion into iteration to prevent stack overflows
- **Transducers**: Composable data transformations with high performance (map, filter, reduce fused)

---

Expand Down
73 changes: 69 additions & 4 deletions benchmark-fp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import {
Ok,
Option,
compose,
composeTransducers,
curry,
filterTransducer,
mapTransducer,
partial,
partialRight,
pipe,
takeTransducer,
uncurry,
} from './src/index.ts';

Expand All @@ -28,7 +32,67 @@ function benchmark<T>(fn: () => T, label: string, operation: string): BenchmarkR

const benchmarks: BenchmarkResult[] = [];

// Function Composition
// ** Test Dataset: 100,000 Numbers **
const numbers = Array.from({ length: 100_000 }, (_, i) => i + 1);
const double = (x: number) => x * 2;
const isEven = (x: number) => x % 2 === 0;
const takeLimit = 5000;

// ** Traditional Array Transformation (map -> filter -> slice) **
benchmarks.push(
benchmark(
() => {
numbers
.filter(isEven) // Keep even numbers
.map(double) // Double them
.slice(0, takeLimit); // Take first `takeLimit` results
},
'Traditional Array Transformation',
'map().filter().slice()',
),
);

// ** Transducer-Based Transformation (reduce) **
const transducer = composeTransducers(
filterTransducer(isEven), // Filter evens
mapTransducer(double), // Double values
takeTransducer(takeLimit), // Take first `takeLimit`
);

benchmarks.push(
benchmark(
() => {
numbers.reduce(
transducer((acc, val) => [...acc, val]),
[],
);
},
'Transducer Approach',
'reduce() with transducers',
),
);

benchmarks.push(
benchmark(
() => {
const result: number[] = [];
numbers.reduce(
transducer((acc, val) => {
acc.push(val); // Mutate instead of spreading
return acc;
}),
result,
);
},
'Transducer Optimized',
'reduce() with transducers (optimized)',
),
);

// ** Display Benchmark Results **
console.table(benchmarks);

// **Existing Function Composition Benchmarks**
benchmarks.push(
benchmark(
() => {
Expand All @@ -55,7 +119,7 @@ benchmarks.push(
),
);

// Currying & Partial Application
// **Currying & Partial Application**
benchmarks.push(
benchmark(
() => {
Expand Down Expand Up @@ -105,7 +169,7 @@ benchmarks.push(
),
);

// Functors: CanApply
// **Functors: CanApply**
benchmarks.push(
benchmark(
() => {
Expand All @@ -119,7 +183,7 @@ benchmarks.push(
),
);

// Monads: Option, Result, and Effect
// **Monads: Option, Result, and Effect**
benchmarks.push(
benchmark(
() => {
Expand Down Expand Up @@ -152,4 +216,5 @@ benchmarks.push(
),
);

// **Display Benchmark Results**
console.table(benchmarks);
59 changes: 59 additions & 0 deletions src/functional/algebraic-data-type/Match-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest';

import { match } from './Match.ts';

describe('match function', () => {
it('should return the correct result for number matching', () => {
const result = match(5, [
[(n) => n === 10, () => 'Exactly ten'],
[(n) => n === 0, () => 'Zero'],
[(n) => n > 10, (n) => `Greater than 10: ${n}`],
[(n) => n > 0, (n) => `Positive: ${n}`],
]);

expect(result).toBe('Positive: 5');
});

it('should return the correct result for string matching', () => {
const result = match('hello', [
[(s) => s === 'world', () => 'Matched world'],
[(s) => s === 'hello', () => 'Matched hello'],
]);

expect(result).toBe('Matched hello');
});

it('should return the correct result for object matching', () => {
const user = { name: 'Alice', age: 25 };

const result = match(user, [
[(u) => u.age > 30, () => 'Older than 30'],
[(u) => u.age >= 25, () => 'Adult'],
]);

expect(result).toBe('Adult');
});

it('should return the correct result for tuple matching', () => {
const point: [number, number] = [3, 4];

const result = match(point, [
[(p) => p[0] === 0 && p[1] === 0, () => 'Origin'],
[(p) => p[0] === 0, () => 'On Y-axis'],
[(p) => p[1] === 0, () => 'On X-axis'],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[(_) => true, () => 'Somewhere else'],
]);

expect(result).toBe('Somewhere else');
});

it('should throw an error if no match is found', () => {
expect(() =>
match(100, [
[(n) => n === 10, () => 'Exactly ten'],
[(n) => n === 0, () => 'Zero'],
]),
).toThrowError('No match found');
});
});
44 changes: 44 additions & 0 deletions src/functional/algebraic-data-type/Match.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* Pattern matching function for TypeScript.
*
* @template T - The type of the value to be matched.
* @param {T} value - The input value to match against patterns.
* @param {Array<[ (value: T) => boolean, (value: T) => any ]>} patterns -
* An array of tuples where:
* - The first element is a predicate function that checks if the pattern applies.
* - The second element is a handler function that executes when the pattern matches.
* @returns {any} - The result of the first matching handler function.
* @throws {Error} If no pattern matches the input value.
*
* @example
* const result = match(5, [
* [(n) => n === 10, () => "Exactly ten"],
* [(n) => n > 0, (n) => `Positive: ${n}`],
* ]);
* console.log(result); // Output: "Positive: 5"
*
* @example
* // Matching against an Option type
* type None = { type: "None" };
* type Some<T> = { type: "Some"; value: T };
* type Option<T> = Some<T> | None;
*
* const None: None = { type: "None" };
* const Some = <T>(value: T): Some<T> => ({ type: "Some", value });
*
* const value: Option<number> = Some(15);
* const message = match(value, [
* [(x) => x.type === "Some", (x) => `Some value: ${x.value}`],
* [(x) => x.type === "None", () => "No value"]
* ]);
* console.log(message); // Output: "Some value: 15"
*/
export function match<T>(value: T, patterns: [(value: T) => boolean, (value: T) => any][]): any {
for (const [predicate, handler] of patterns) {
if (predicate(value)) {
return handler(value);
}
}
throw new Error('No match found');
}
100 changes: 100 additions & 0 deletions src/functional/algebraic-data-type/MatchProperty-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as fc from 'fast-check';
import { describe, expect, it } from 'vitest';

import { match } from './Match.ts';

describe('match function - property-based testing', () => {
it('should return the expected result for positive numbers', () => {
fc.assert(
fc.property(fc.integer({ min: 1 }), (n) => {
const result = match(n, [[(x) => x > 0, (x) => `Positive: ${x}`]]);
expect(result).toBe(`Positive: ${n}`);
}),
);
});

it('should correctly match string patterns', () => {
fc.assert(
fc.property(fc.string(), (s) => {
const result = match(s, [
[(x) => x.startsWith('A'), () => 'Starts with A'],
[(x) => x.endsWith('Z'), () => 'Ends with Z'],
[(x) => x.length === 0, () => 'Empty string'],
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[(_) => true, () => 'Fallback'],
]);

// Property: The result should be a known output
expect(['Starts with A', 'Ends with Z', 'Empty string', 'Fallback']).toContain(
result,
);
}),
);
});

it('should correctly match objects', () => {
fc.assert(
fc.property(
fc.record({
age: fc.integer({ min: 0, max: 120 }),
name: fc.string(),
}),
(user) => {
const result = match(user, [
[(u) => u.age < 18, () => 'Minor'],
[(u) => u.age >= 18, () => 'Adult'],
]);

expect(['Minor', 'Adult']).toContain(result);
},
),
);
});

it('should always return a value from the match cases', () => {
fc.assert(
fc.property(fc.anything(), (randomValue) => {
try {
match(randomValue, [
[(x) => typeof x === 'number', (x) => `Number: ${x}`],
[(x) => typeof x === 'string', (x) => `String: ${x}`],
]);
} catch (e) {
expect(e).toBeInstanceOf(Error);
}
}),
);
});

it('should throw an error when no match is found', () => {
fc.assert(
fc.property(fc.anything(), (randomValue) => {
expect(() =>
match(randomValue, [
// eslint-disable-next-line @typescript-eslint/no-unused-vars
[(x) => false, () => 'This never matches'], // Always false, so match() should always fail
]),
).toThrowError('No match found');
}),
);
});

it('should throw an error when no match is found', () => {
fc.assert(
fc.property(fc.anything(), (randomValue) => {
const patterns: [(value: any) => boolean, (value: any) => any][] = [
[(x) => typeof x === 'number', () => "It's a number"],
[(x) => typeof x === 'string', () => "It's a string"],
];

// Check if any predicate matches
const hasMatch = patterns.some(([predicate]) => predicate(randomValue));

if (!hasMatch) {
expect(() => match(randomValue, patterns)).toThrowError('No match found');
}
}),
);
});
});
8 changes: 8 additions & 0 deletions src/functional/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@ export { compose, pipe } from './composition/Composition.ts';
export { curry, uncurry } from './curry/Curry.ts';
export { partial, partialRight } from './partial/Partial.ts';
export { CanApply } from './functors/CanApply.ts';
export { match } from './algebraic-data-type/Match.ts';
export { lens, isoLens, optionalLens, traversalLens } from './lens/Lens.ts';
export {
composeTransducers,
mapTransducer,
filterTransducer,
takeTransducer,
} from './transducers/Transducers.ts';
export * from './monads/index.ts';
Loading
Loading