-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from helabenkhalfallah/feature/functional-patterns
[FEATURE]: add functional programming patterns
- Loading branch information
Showing
31 changed files
with
2,394 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.