Skip to content

Commit

Permalink
Merge pull request #3 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 ed0516c + 16a84a6 commit b03def7
Show file tree
Hide file tree
Showing 19 changed files with 1,239 additions and 5 deletions.
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

0 comments on commit b03def7

Please sign in to comment.