From b646e4b95c4f0452484f03e106555374faba10c5 Mon Sep 17 00:00:00 2001 From: helabenkhalfallah Date: Fri, 14 Feb 2025 11:36:03 +0100 Subject: [PATCH] [FEATURE]: add functional programming patterns --- README.md | 143 +++++--- benchmark-fp.ts | 155 +++++++++ package.json | 4 +- pnpm-lock.yaml | 14 + .../trees/bst/BinarySearchTree-test.ts | 2 +- .../trees/red-black/RedBlackTree-test.ts | 4 +- src/data-structures/trees/trie/Trie-test.ts | 2 +- .../composition/Composition-test.ts | 103 ++++++ src/functional/composition/Composition.ts | 40 +++ .../composition/CompositionProperty-test.ts | 84 +++++ src/functional/curry/Curry-test.ts | 53 +++ src/functional/curry/Curry.ts | 54 +++ src/functional/curry/CurryProperty-test.ts | 78 +++++ src/functional/functors/CanApply-test.ts | 82 +++++ src/functional/functors/CanApply.ts | 31 ++ .../functors/CanApplyProperty-test.ts | 87 +++++ src/functional/index.ts | 5 + src/functional/monads/Effect-test.ts | 57 +++ src/functional/monads/Effect.ts | 141 ++++++++ src/functional/monads/EffectProperty-test.ts | 93 +++++ src/functional/monads/Option-test.ts | 101 ++++++ src/functional/monads/Option.ts | 284 +++++++++++++++ src/functional/monads/OptionProperty-test.ts | 106 ++++++ src/functional/monads/Result-test.ts | 123 +++++++ src/functional/monads/Result.ts | 328 ++++++++++++++++++ src/functional/monads/ResultProperty-test.ts | 94 +++++ src/functional/monads/index.ts | 3 + src/functional/partial/Partial-test.ts | 63 ++++ src/functional/partial/Partial.ts | 43 +++ .../partial/PartialProperty-test.ts | 74 ++++ src/index.ts | 1 + 31 files changed, 2394 insertions(+), 58 deletions(-) create mode 100644 benchmark-fp.ts create mode 100644 src/functional/composition/Composition-test.ts create mode 100644 src/functional/composition/Composition.ts create mode 100644 src/functional/composition/CompositionProperty-test.ts create mode 100644 src/functional/curry/Curry-test.ts create mode 100644 src/functional/curry/Curry.ts create mode 100644 src/functional/curry/CurryProperty-test.ts create mode 100644 src/functional/functors/CanApply-test.ts create mode 100644 src/functional/functors/CanApply.ts create mode 100644 src/functional/functors/CanApplyProperty-test.ts create mode 100644 src/functional/index.ts create mode 100644 src/functional/monads/Effect-test.ts create mode 100644 src/functional/monads/Effect.ts create mode 100644 src/functional/monads/EffectProperty-test.ts create mode 100644 src/functional/monads/Option-test.ts create mode 100644 src/functional/monads/Option.ts create mode 100644 src/functional/monads/OptionProperty-test.ts create mode 100644 src/functional/monads/Result-test.ts create mode 100644 src/functional/monads/Result.ts create mode 100644 src/functional/monads/ResultProperty-test.ts create mode 100644 src/functional/monads/index.ts create mode 100644 src/functional/partial/Partial-test.ts create mode 100644 src/functional/partial/Partial.ts create mode 100644 src/functional/partial/PartialProperty-test.ts diff --git a/README.md b/README.md index 2eb54b1..c9b36e1 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,16 @@ Whether you're building a lightweight application or handling large datasets, ** - **Trees**: AVL Tree, Red-Black Tree, Binary Search Tree, B-Tree, Trie - **Probabilistic Structures**: Bloom Filter, HyperLogLog, CountMinSketch, SkipList, MinHash, SimHash, TDigest +### 📊 Functional Programming (FP) + +- **Composition**: Function composition for declarative programming +- **Currying**: Transforming functions into unary functions +- **Functors**: CanApply for safe function application +- **Monads**: + - **Option**: Safe handling of optional values (Some, None) + - **Result**: Error handling without exceptions (Ok, Err) + - **Effect**: Deferred computations with error safety + --- ## Benchmarks @@ -183,6 +193,28 @@ Whether you're building a lightweight application or handling large datasets, ** - Linear Search: Suited for small, unsorted datasets where search is infrequent. - Ternary Search: Good for distinct, ordered data ranges, often in optimization or game algorithms. +## 🏎️ Functional Programming (FP) Benchmarks + +Functional programming utilities are designed for **safe, composable, and efficient** function transformations. + +| **Function** | **Operation** | **Time (ms)** | +|------------------|--------------------------------|----------------------| +| `compose` | Function composition | 0.0565 | +| `pipe` | Function piping | 0.0371 | +| `curry` | Currying | 0.0508 | +| `partial` | Partial Application | 0.0302 | +| `partialRight` | Partial Right Application | 0.0352 | +| `uncurry` | Uncurrying | 0.0591 | +| `CanApply` | Functor Mapping | 0.0694 | +| `Option` | Option Mapping (Monad) | 0.0767 | +| `Result` | Result Mapping (Monad) | 0.0649 | +| `Effect` | Effect Execution (Monad) | 0.0578 | + +- **Function composition (`compose`, `pipe`) is highly efficient**, enabling declarative and reusable transformations. +- **Currying (`curry`, `uncurry`) and partial application (`partial`, `partialRight`) improve function modularity** while maintaining performance. +- **Functors and Monads (`CanApply`, `Option`, `Result`, `Effect`) ensure safety and composability**, with reasonable execution times. +- **Effect handling (`Effect`) provides error resilience**, making it ideal for managing side effects safely. + --- ## 🛠️ Usage @@ -225,6 +257,11 @@ import { BinarySearchTree, RedBlackTree, Trie, + Option, + Effect, + compose, + curry, + CanApply } from 'dsa-toolbox'; binarySearch(sortedData, target, { compareFn: (a, b) => a - b, isSorted: true }); @@ -243,6 +280,19 @@ avlTree.insert(5); avlTree.insert(4); avlTree.insert(15); avlTree.search(10); + + +const double = (x: number) => x * 2; +const increment = (x: number) => x + 1; +const doubleThenIncrement = compose(increment, double); +console.log(doubleThenIncrement(3)); // 7 + +const safeValue = CanApply(10) + .map((x) => x * 2) + .map((x) => `Result: ${x}`) + .getValue(); + +console.log(safeValue); // "Result: 20" ``` More usage examples can be found here: @@ -268,59 +318,46 @@ For detailed explanations of each data structure and algorithm, please visit: ## Code coverage -| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | -|--------------------------------------------|---------|----------|---------|---------|--------------------------------------------------------------------------------------------| -| All files | 90.84 | 90.44 | 96.34 | 90.84 | | -| algorithms/search | 96.9 | 92.85 | 100 | 96.9 | | -| BinarySearch.ts | 100 | 100 | 100 | 100 | | -| ExponentialSearch.ts | 100 | 87.5 | 100 | 100 | 70 | -| HybridSearch.ts | 93.33 | 87.5 | 100 | 93.33 | 121-122 | -| LinearSearch.ts | 100 | 100 | 100 | 100 | | -| TernarySearch.ts | 96 | 92.3 | 100 | 96 | 82 | -| algorithms/sort | 99.44 | 95.23 | 100 | 99.44 | | -| HeapSort.ts | 100 | 100 | 100 | 100 | | -| MergeSort.ts | 100 | 100 | 100 | 100 | | -| TimSort.ts | 99.24 | 93.47 | 100 | 99.24 | 128 | -| commons | 85.71 | 100 | 75 | 85.71 | | -| ComparableNode.ts | 85.71 | 100 | 75 | 85.71 | 50-51 | -| data-structures/cache | 100 | 94.59 | 100 | 100 | | -| LFU.ts | 100 | 90.9 | 100 | 100 | 75,148 | -| LRU.ts | 100 | 100 | 100 | 100 | | -| data-structures/heaps | 97.14 | 93.87 | 88.88 | 97.14 | | -| MaxHeap.ts | 97.14 | 96 | 88.88 | 97.14 | 138-139 | -| MinHeap.ts | 97.14 | 91.66 | 88.88 | 97.14 | 139-140 | -| data-structures/linked-list | 97.04 | 89.06 | 100 | 97.04 | | -| DoublyLinkedList.ts | 95.65 | 88.57 | 100 | 95.65 | 87-89,91 | -| LinkedList.ts | 98.7 | 89.65 | 100 | 98.7 | 75 | -| data-structures/probabilistic/cardinality | 84.61 | 87.5 | 87.5 | 84.61 | | -| HyperLogLog.ts | 84.61 | 87.5 | 87.5 | 84.61 | 61-63,75-81 | -| data-structures/probabilistic/frequency | 100 | 100 | 100 | 100 | | -| CountMinSketch.ts | 100 | 100 | 100 | 100 | | -| data-structures/probabilistic/membership | 100 | 100 | 100 | 100 | | -| BloomFilter.ts | 100 | 100 | 100 | 100 | | -| data-structures/probabilistic/ordered | 97.87 | 97.22 | 100 | 97.87 | | -| SkipList.ts | 97.87 | 97.22 | 100 | 97.87 | 102-103 | -| data-structures/probabilistic/quantile | 97.97 | 91.42 | 100 | 97.97 | | -| TDigest.ts | 97.97 | 91.42 | 100 | 97.97 | 61,85 | -| data-structures/probabilistic/similarity | 100 | 100 | 100 | 100 | | -| MinHash.ts | 100 | 100 | 100 | 100 | | -| SimHash.ts | 100 | 100 | 100 | 100 | | -| data-structures/queue | 100 | 100 | 100 | 100 | | -| Queue.ts | 100 | 100 | 100 | 100 | | -| data-structures/stack | 100 | 100 | 100 | 100 | | -| Stack.ts | 100 | 100 | 100 | 100 | | -| data-structures/treaps | 82.41 | 88.88 | 92.85 | 82.41 | | -| Treap.ts | 82.41 | 88.88 | 92.85 | 82.41 | 84-85,111-115,163,168-176 | -| data-structures/trees/avl | 90.62 | 84.31 | 100 | 90.62 | | -| AVLTree.ts | 90.62 | 84.31 | 100 | 90.62 | 118-119,169,196-198,201-203 | -| data-structures/trees/b-tree | 68.71 | 75 | 76.47 | 68.71 | | -| BTree.ts | 68.71 | 75 | 76.47 | 68.71 | 67,114-115,119-120,146-147,169-171,182-192,203-208,220-221,232-243,252-261,270-279,294-295 | -| data-structures/trees/bst | 93.4 | 93.61 | 100 | 93.4 | | -| BinarySearchTree.ts | 93.4 | 93.61 | 100 | 93.4 | 132-134,137-139 | -| data-structures/trees/red-black | 76.07 | 81.81 | 100 | 76.07 | | -| RedBlackTree.ts | 76.07 | 81.81 | 100 | 76.07 | 107-114,274,278-279,318-344,347-373,379,395-396,398-399,442-443 | -| data-structures/trees/trie | 100 | 96 | 100 | 100 | | -| Trie.ts | 100 | 96 | 100 | 100 | 89 | +| **Metric** | **Coverage** | +|------------|-------------| +| **Statements** | 91.38% | +| **Branches** | 91.11% | +| **Functions** | 96.03% | +| **Lines** | 91.38% | + + +| **File/Module** | **% Stmts** | **% Branch** | **% Funcs** | **% Lines** | **Uncovered Line #s** | +|--------------------------------------|------------|-------------|------------|------------|----------------------| +| **All files** | 91.38 | 91.11 | 96.03 | 91.38 | | +| **Algorithms / Search** | 96.9 | 92.85 | 100 | 96.9 | | +| `BinarySearch.ts` | 100 | 100 | 100 | 100 | | +| `ExponentialSearch.ts` | 100 | 87.5 | 100 | 100 | 70 | +| `HybridSearch.ts` | 93.33 | 87.5 | 100 | 93.33 | 121-122 | +| `LinearSearch.ts` | 100 | 100 | 100 | 100 | | +| `TernarySearch.ts` | 96 | 92.3 | 100 | 96 | 82 | +| **Algorithms / Sort** | 99.44 | 95.23 | 100 | 99.44 | | +| `HeapSort.ts` | 100 | 100 | 100 | 100 | | +| `MergeSort.ts` | 100 | 100 | 100 | 100 | | +| `TimSort.ts` | 99.24 | 93.47 | 100 | 99.24 | 128 | +| **Common Utilities** | 85.71 | 100 | 75 | 85.71 | | +| `ComparableNode.ts` | 85.71 | 100 | 75 | 85.71 | 50-51 | +| **Data Structures / Caching** | 100 | 94.59 | 100 | 100 | | +| `LFU.ts` | 100 | 90.9 | 100 | 100 | 75,148 | +| `LRU.ts` | 100 | 100 | 100 | 100 | | +| **Data Structures / Heaps** | 97.14 | 93.87 | 88.88 | 97.14 | | +| `MaxHeap.ts` | 97.14 | 96 | 88.88 | 97.14 | 138-139 | +| `MinHeap.ts` | 97.14 | 91.66 | 88.88 | 97.14 | 139-140 | +| **Data Structures / Linked Lists** | 97.04 | 89.06 | 100 | 97.04 | | +| `DoublyLinkedList.ts` | 95.65 | 88.57 | 100 | 95.65 | 87-89,91 | +| `LinkedList.ts` | 98.7 | 89.65 | 100 | 98.7 | 75 | +| **Functional Programming (FP)** | **96.38** | **100** | **93.61** | **96.38** | | +| `Composition.ts` | 77.77 | 87.5 | 100 | 77.77 | 18-19 | +| `Curry.ts` | 91.3 | 88.88 | 100 | 91.3 | 49-50 | +| `CanApply.ts` | 100 | 85.71 | 100 | 100 | 29 | +| `Effect.ts` | 100 | 100 | 100 | 100 | | +| `Option.ts` | 96.87 | 100 | 95 | 96.87 | 15-16 | +| `Result.ts` | 94.44 | 100 | 90.9 | 94.44 | 16-17,26-27 | +| `Partial.ts` | 100 | 100 | 100 | 100 | | --- diff --git a/benchmark-fp.ts b/benchmark-fp.ts new file mode 100644 index 0000000..ab60ae1 --- /dev/null +++ b/benchmark-fp.ts @@ -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(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); diff --git a/package.json b/package.json index cbf137b..e030ab3 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8cd99d..5e367d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ devDependencies: eslint-plugin-prettier: specifier: '=5.2.1' version: 5.2.1(eslint-config-prettier@9.1.0)(eslint@9.0.0)(prettier@3.4.2) + fast-check: + specifier: '=3.23.2' + version: 3.23.2 husky: specifier: '=9.1.6' version: 9.1.6 @@ -1527,6 +1530,13 @@ packages: strip-final-newline: 3.0.0 dev: true + /fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + dependencies: + pure-rand: 6.1.0 + dev: true + /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -2265,6 +2275,10 @@ packages: engines: {node: '>=6'} dev: true + /pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + dev: true + /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true diff --git a/src/data-structures/trees/bst/BinarySearchTree-test.ts b/src/data-structures/trees/bst/BinarySearchTree-test.ts index 3bd283d..1fb1677 100644 --- a/src/data-structures/trees/bst/BinarySearchTree-test.ts +++ b/src/data-structures/trees/bst/BinarySearchTree-test.ts @@ -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', () => { diff --git a/src/data-structures/trees/red-black/RedBlackTree-test.ts b/src/data-structures/trees/red-black/RedBlackTree-test.ts index 21279b2..bd4d62c 100644 --- a/src/data-structures/trees/red-black/RedBlackTree-test.ts +++ b/src/data-structures/trees/red-black/RedBlackTree-test.ts @@ -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', () => { diff --git a/src/data-structures/trees/trie/Trie-test.ts b/src/data-structures/trees/trie/Trie-test.ts index 76fe395..bc14c75 100644 --- a/src/data-structures/trees/trie/Trie-test.ts +++ b/src/data-structures/trees/trie/Trie-test.ts @@ -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', () => { diff --git a/src/functional/composition/Composition-test.ts b/src/functional/composition/Composition-test.ts new file mode 100644 index 0000000..9c95a4d --- /dev/null +++ b/src/functional/composition/Composition-test.ts @@ -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); + }); +}); diff --git a/src/functional/composition/Composition.ts b/src/functional/composition/Composition.ts new file mode 100644 index 0000000..2da34bc --- /dev/null +++ b/src/functional/composition/Composition.ts @@ -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(...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(...fns: Array<(arg: T) => T>): (arg: T) => T { + return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg); +} diff --git a/src/functional/composition/CompositionProperty-test.ts b/src/functional/composition/CompositionProperty-test.ts new file mode 100644 index 0000000..ca67f61 --- /dev/null +++ b/src/functional/composition/CompositionProperty-test.ts @@ -0,0 +1,84 @@ +import * as fc from 'fast-check'; +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}!`; + + // Identity Law: compose(id) and pipe(id) should be equivalent to identity function + it('should satisfy composition identity property', () => { + fc.assert( + fc.property(fc.string(), (input) => { + const id = (x: string) => x; + expect(compose(id)(input)).toBe(input); + expect(pipe(id)(input)).toBe(input); + }), + ); + }); + + // Associativity: compose(f, compose(g, h)) === compose(compose(f, g), h) + it('should be associative', () => { + fc.assert( + fc.property(fc.string(), (input) => { + const f = trim; + const g = toUpperCase; + const h = exclaim; + + // Associativity Law + expect(compose(f, compose(g, h))(input)).toBe(compose(compose(f, g), h)(input)); + expect(pipe(pipe(f, g), h)(input)).toBe(pipe(f, pipe(g, h))(input)); + }), + ); + }); + + // Idempotency: If a function is idempotent (f(f(x)) === f(x)), compose(f, f) should be equivalent to f + it('should satisfy idempotency for idempotent functions', () => { + fc.assert( + fc.property(fc.string(), (input) => { + const idempotentTrim = (s: string) => s.trim(); + expect(compose(idempotentTrim, idempotentTrim)(input)).toBe(idempotentTrim(input)); + expect(pipe(idempotentTrim, idempotentTrim)(input)).toBe(idempotentTrim(input)); + }), + ); + }); + + // Composition with multiple transformations should be equivalent to applying them individually + it('should maintain transformation consistency', () => { + fc.assert( + fc.property(fc.string(), (input) => { + const transformedDirectly = exclaim(toUpperCase(trim(input))); + const transformedViaCompose = compose(exclaim, toUpperCase, trim)(input); + const transformedViaPipe = pipe(trim, toUpperCase, exclaim)(input); + + expect(transformedViaCompose).toBe(transformedDirectly); + expect(transformedViaPipe).toBe(transformedDirectly); + }), + ); + }); + + // Length Preservation: If functions do not change length, compose(f, g) should preserve the input length + it('should preserve string length if transformations do not add/remove characters', () => { + fc.assert( + fc.property(fc.string(), (input) => { + const f = (s: string) => s.replace(/\s/g, '_'); // Replace spaces with underscores (preserves length) + const g = (s: string) => s.toLowerCase(); // Does not change length + const transformed = compose(f, g)(input); + expect(transformed.length).toBe(input.length); + }), + ); + }); + + // Involution: If a function is its own inverse (f(f(x)) = x), then compose(f, f) should return the original input + it('should satisfy involution for inverse functions', () => { + fc.assert( + fc.property(fc.string(), (input) => { + const reverse = (s: string) => s.split('').reverse().join(''); + expect(compose(reverse, reverse)(input)).toBe(input); + expect(pipe(reverse, reverse)(input)).toBe(input); + }), + ); + }); +}); diff --git a/src/functional/curry/Curry-test.ts b/src/functional/curry/Curry-test.ts new file mode 100644 index 0000000..2c81b58 --- /dev/null +++ b/src/functional/curry/Curry-test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; + +import { curry, uncurry } from './Curry.ts'; + +describe('curry', () => { + it('should correctly curry a function', () => { + const add = (a: number, b: number, c: number) => a + b + c; + const curriedAdd = curry(add); + + expect(curriedAdd(1)(2)(3)).toBe(6); + expect(curriedAdd(1, 2)(3)).toBe(6); + expect(curriedAdd(1)(2, 3)).toBe(6); + expect(curriedAdd(1, 2, 3)).toBe(6); + }); + + it('should handle single-argument functions correctly', () => { + const identity = (x: number) => x; + const curriedIdentity = curry(identity); + expect(curriedIdentity(5)).toBe(5); + }); + + it('should allow partial application', () => { + const multiply = (a: number, b: number) => a * b; + const curriedMultiply = curry(multiply); + + const multiplyBy2 = curriedMultiply(2); + expect(multiplyBy2(3)).toBe(6); + expect(multiplyBy2(4)).toBe(8); + }); +}); + +describe('uncurry', () => { + it('should correctly uncurry a curried function', () => { + const curriedAdd = (a: number) => (b: number) => (c: number) => a + b + c; + const uncurriedAdd = uncurry(curriedAdd); + + expect(uncurriedAdd(1, 2, 3)).toBe(6); + }); + + it('should correctly uncurry a single-argument function', () => { + const curriedIdentity = (x: number) => x; + const uncurriedIdentity = uncurry(curriedIdentity); + + expect(uncurriedIdentity(5)).toBe(5); + }); + + it('should correctly handle functions with different arities', () => { + const curriedConcat = (a: string) => (b: string) => (c: string) => `${a}-${b}-${c}`; + const uncurriedConcat = uncurry(curriedConcat); + + expect(uncurriedConcat('A', 'B', 'C')).toBe('A-B-C'); + }); +}); diff --git a/src/functional/curry/Curry.ts b/src/functional/curry/Curry.ts new file mode 100644 index 0000000..8412286 --- /dev/null +++ b/src/functional/curry/Curry.ts @@ -0,0 +1,54 @@ +type Curried = T extends [infer A, ...infer Rest] // Extract first argument (A) and remaining (Rest) + ? Rest extends [] // If no remaining args, return (A) => R + ? (a: A) => R + : (a: A, ...rest: Partial) => Curried // Allows multiple arguments in each call + : never; + +/** + * Converts a function that takes multiple arguments into a curried function. + * + * @template T - The tuple type of the function's arguments. + * @template R - The return type of the function. + * @param {(...args: T) => R} fn - The function to curry. + * @returns {Curried} - A curried version of the function. + * + * @example + * const add = (a: number, b: number, c: number) => a + b + c; + * const curriedAdd = curry(add); + * console.log(curriedAdd(1)(2)(3)); // Output: 6 + * console.log(curriedAdd(1, 2)(3)); // Output: 6 + * console.log(curriedAdd(1)(2, 3)); // Output: 6 + * console.log(curriedAdd(1, 2, 3)); // Output: 6 + */ +export function curry(fn: (...args: T) => R): Curried { + return function curried(...args: unknown[]): unknown { + if (args.length >= fn.length) { + return fn(...(args as T)); // Return the result when all arguments are collected + } + + return (...nextArgs: unknown[]) => curried(...args, ...nextArgs); // Partial application + } as Curried; +} + +/** + * Transforms a curried function into a function that accepts all its arguments at once. + * Works by iteratively applying arguments to the provided curried function. + * + * @param fn The curried function to be transformed. Should accept a sequence of functions eventually producing a result. + * @return A function that takes all the arguments expected by the curried function at once and returns the final result. + */ +export function uncurry( + fn: (...args: unknown[]) => unknown, // Allows TypeScript to infer function structure +): (...args: T) => R { + return (...args: T) => { + let result: unknown = fn; + for (const arg of args) { + if (typeof result === 'function') { + result = result(arg); + } else { + throw new Error('Unexpected function application in uncurry.'); + } + } + return result as R; + }; +} diff --git a/src/functional/curry/CurryProperty-test.ts b/src/functional/curry/CurryProperty-test.ts new file mode 100644 index 0000000..eeca341 --- /dev/null +++ b/src/functional/curry/CurryProperty-test.ts @@ -0,0 +1,78 @@ +import * as fc from 'fast-check'; +import { describe, expect, it } from 'vitest'; + +import { curry, uncurry } from './Curry.ts'; + +describe('Functional Programming: curry & uncurry', () => { + it('should satisfy curry(uncurry(fn)) === fn', () => { + fc.assert( + fc.property( + fc.func(fc.integer()), // Generates random functions returning integers + (fn) => { + const curriedFn = curry(fn); + const uncurriedFn = uncurry(curriedFn); + return fc.assert( + fc.property(fc.integer(), (x) => { + expect(uncurriedFn(x)).toBe(fn(x)); + }), + ); + }, + ), + ); + }); + + it('should be associative (partial application order should not affect result)', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), (a, b) => { + const add = (x: number, y: number) => x + y; + const curriedAdd = curry(add); + + expect(curriedAdd(a)(b)).toBe(add(a, b)); + }), + ); + }); + + it('should satisfy uncurry(curry(fn)) === fn for multi-argument functions', () => { + fc.assert( + fc.property( + fc.func<[number, number], number>(fc.integer()), // Generates valid functions + (fn) => { + // Ensure that we only test functions that are fully applicable when curried + const wrappedFn = (a: number, b: number) => fn(a, b); + + const curriedFn = curry(wrappedFn); + const uncurriedFn = uncurry(curriedFn); + + fc.assert( + fc.property(fc.integer(), fc.integer(), (x, y) => { + expect(typeof uncurriedFn).toBe('function'); // Ensures correctness + expect(uncurriedFn(x, y)).toBe(wrappedFn(x, y)); // Ensures full applicability + }), + ); + }, + ), + ); + }); + + it('should return the correct output when uncurry is applied to a deeply nested function', () => { + fc.assert( + fc.property(fc.integer(), fc.integer(), fc.integer(), (a, b, c) => { + const curriedAdd = (x: number) => (y: number) => (z: number) => x + y + z; + const uncurriedAdd = uncurry(curriedAdd); + + expect(uncurriedAdd(a, b, c)).toBe(a + b + c); + }), + ); + }); + + it('should correctly handle different arities', () => { + fc.assert( + fc.property(fc.string(), fc.string(), fc.string(), (a, b, c) => { + const curriedConcat = (x: string) => (y: string) => (z: string) => `${x}-${y}-${z}`; + const uncurriedConcat = uncurry(curriedConcat); + + expect(uncurriedConcat(a, b, c)).toBe(`${a}-${b}-${c}`); + }), + ); + }); +}); diff --git a/src/functional/functors/CanApply-test.ts b/src/functional/functors/CanApply-test.ts new file mode 100644 index 0000000..a514b4e --- /dev/null +++ b/src/functional/functors/CanApply-test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it } from 'vitest'; + +import { CanApply } from './CanApply.ts'; + +describe('CanApply Functor', () => { + it('should apply valid operations safely', () => { + const result = CanApply(5) + .map((x) => x * 2) + .map((x) => x + 3) + .getValue(); + + expect(result).toBe(13); // 5 → 10 → 13 + }); + + it('should transform type (number → string)', () => { + const result = CanApply(42) + .map((x) => x.toString()) + .map((x) => x + '!') + .getValue(); + + expect(result).toBe('42!'); // Converts number → string + }); + + it('should transform object → array', () => { + const result = CanApply({ name: 'Alice', age: 25 }) + .map(Object.entries) // Convert object → array + .getValue(); + + expect(result).toEqual([ + ['name', 'Alice'], + ['age', 25], + ]); + }); + + it('should handle implicit coercion correctly', () => { + const result = CanApply(5) + .map((x) => x + '!') // JavaScript coerces `5` into `"5"` + .getValue(); + + expect(result).toBe('5!'); // Expected: `"5!"`, NOT `null` + }); + + it('should handle JSON parsing safely', () => { + const validJSON = CanApply('{"valid": true}') + .map(JSON.parse) + .map((obj) => obj.valid) + .getValue(); + + expect(validJSON).toBe(true); // Parsed successfully + + const invalidJSON = CanApply('{"invalid"') // Broken JSON + .map(JSON.parse) // Fails safely + .getValue(); + + expect(invalidJSON).toBe(null); + }); + + it('should satisfy functor identity law', () => { + const value = 10; + const result = CanApply(value) + .map((x) => x) // Identity function + .getValue(); + + expect(result).toBe(value); // Identity: CanApply(x).map(id) === CanApply(x) + }); + + it('should satisfy functor composition law', () => { + const f = (x: number) => x * 2; + const g = (x: number) => x + 3; + + const composed = CanApply(5) + .map((x) => g(f(x))) // Direct composition + .getValue(); + + const separateMapping = CanApply(5) + .map(f) + .map(g) // Separate function application + .getValue(); + + expect(composed).toBe(separateMapping); // Functor law holds: map(f ∘ g) ≡ map(f) ∘ map(g) + }); +}); diff --git a/src/functional/functors/CanApply.ts b/src/functional/functors/CanApply.ts new file mode 100644 index 0000000..3dae6c4 --- /dev/null +++ b/src/functional/functors/CanApply.ts @@ -0,0 +1,31 @@ +type CanApply = { + map: (fn: (value: T) => U) => CanApply; + getValue: () => T | null; +}; + +/** + * A functor that safely applies transformations to a value, preventing errors while allowing type changes. + * + * @param value - The initial value. + * @returns A `CanApply` instance. + * + * @example + * const result = CanApply(5) + * .map(x => x * 2) // 10 + * .map(x => x.toString()) // "10" + * .getValue(); + * console.log(result); // "10" + */ +export function CanApply(value: T) { + return { + map: (fn: (x: T) => U) => { + try { + const result = fn(value); + return CanApply(result !== undefined ? result : null); + } catch { + return CanApply(null); + } + }, + getValue: () => (value !== undefined ? value : null), + }; +} diff --git a/src/functional/functors/CanApplyProperty-test.ts b/src/functional/functors/CanApplyProperty-test.ts new file mode 100644 index 0000000..57e812b --- /dev/null +++ b/src/functional/functors/CanApplyProperty-test.ts @@ -0,0 +1,87 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as fc from 'fast-check'; +import { describe, expect, it } from 'vitest'; + +import { CanApply } from './CanApply.ts'; + +describe('CanApply (Property-based Tests)', () => { + it('should satisfy identity law', () => { + fc.assert( + fc.property(fc.anything(), (x) => { + const identity = (v: unknown) => v; + expect(CanApply(x).map(identity).getValue()).toStrictEqual(CanApply(x).getValue()); + }), + ); + }); + + it('should satisfy composition law', () => { + fc.assert( + fc.property(fc.integer(), (x) => { + const f = (n: number) => n * 2; + const g = (n: number) => n + 10; + + const left = CanApply(x).map(f).map(g).getValue(); + const right = CanApply(x) + .map((y) => g(f(y))) + .getValue(); + + expect(left).toStrictEqual(right); + }), + ); + }); + + it('should not throw errors on invalid operations', () => { + fc.assert( + fc.property(fc.anything(), (x) => { + const result = CanApply(x) + .map((y) => { + if (typeof y === 'number') return (y as any).nonExistentMethod(); + if (typeof y === 'object') return (y as any).invalidAccess; + return y; + }) + .getValue(); + + if ( + x === null || + x === undefined || + (typeof x === 'number' && !(x as any).nonExistentMethod) || // Error case + (typeof x === 'object' && (x as any).invalidAccess === undefined) // Error case + ) { + expect(result).toBe(null); + } else { + expect(result).toStrictEqual(x); + } + }), + ); + }); + + it('should preserve type transformations', () => { + fc.assert( + fc.property(fc.integer(), (x) => { + const result = CanApply(x) + .map((n) => n.toString()) // Convert number to string + .map((s) => s + '!') // Append exclamation + .getValue(); + + expect(typeof result).toBe('string'); + expect(result).toStrictEqual(`${x}!`); + }), + ); + }); + + it('should correctly apply functions only when valid', () => { + fc.assert( + fc.property(fc.oneof(fc.integer(), fc.string(), fc.boolean()), (x) => { + const result = CanApply(x) + .map((y) => (typeof y === 'number' ? y * 2 : y)) // Multiply only numbers + .getValue(); + + if (typeof x === 'number') { + expect(result).toStrictEqual(x * 2); + } else { + expect(result).toStrictEqual(x); // Other types remain unchanged + } + }), + ); + }); +}); diff --git a/src/functional/index.ts b/src/functional/index.ts new file mode 100644 index 0000000..506497e --- /dev/null +++ b/src/functional/index.ts @@ -0,0 +1,5 @@ +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 * from './monads/index.ts'; diff --git a/src/functional/monads/Effect-test.ts b/src/functional/monads/Effect-test.ts new file mode 100644 index 0000000..8610221 --- /dev/null +++ b/src/functional/monads/Effect-test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest'; + +import { Effect } from './Effect.ts'; +import { Err, Ok } from './Result.ts'; + +describe('Effect', () => { + it('should execute a successful effect', () => { + const effect = Effect(() => 42); + expect(effect.run()).toStrictEqual(new Ok(42)); + }); + + it('should catch errors and return Err', () => { + const effect = Effect(() => { + throw new Error('Boom!'); + }); + expect(effect.run()).toStrictEqual(new Err(new Error('Boom!'))); + }); + + it('should transform the result with map', () => { + const effect = Effect(() => 10).map((x) => x * 2); + expect(effect.run()).toStrictEqual(new Ok(20)); + }); + + it('should chain effects with flatMap', () => { + const effect = Effect(() => 5).flatMap((x) => Effect(() => x + 10)); + expect(effect.run()).toStrictEqual(new Ok(15)); + }); + + it('should recover from errors with recover', () => { + const effect = Effect(() => { + throw new Error('Failure'); + }).recover(() => 0); + + expect(effect.run()).toStrictEqual(new Ok(0)); // ✅ Now TypeScript is happy! + }); + + it('should not modify success values in recover', () => { + const effect = Effect(() => 42).recover(() => 0); + expect(effect.run()).toStrictEqual(new Ok(42)); + }); + + it('should work with side effects', () => { + let value = 0; + const effect = Effect(() => { + value = 100; + return value; + }); + + expect(effect.run()).toStrictEqual(new Ok(100)); + expect(value).toBe(100); + }); + + it('should correctly handle empty effect', () => { + const effect = Effect(() => undefined); + expect(effect.run()).toStrictEqual(new Ok(undefined)); + }); +}); diff --git a/src/functional/monads/Effect.ts b/src/functional/monads/Effect.ts new file mode 100644 index 0000000..71f15d7 --- /dev/null +++ b/src/functional/monads/Effect.ts @@ -0,0 +1,141 @@ +import { Err, Ok, Result } from './Result.ts'; + +/** + * Represents a deferred computation that can fail safely. + * It encapsulates a lazy computation that may throw an error, allowing safe composition. + * + * @template T - The type of the successful value. + * @template E - The type of the error. + */ +export type Effect = { + /** + * Transforms the successful result of the effect. + * + * @template U - The transformed value type. + * @param {(value: T) => U} fn - Function to apply on success. + * @returns {Effect} - A new Effect with transformed data. + * + * @example + * const effect = Effect(() => 10).map(x => x * 2); + * console.log(effect.run()); // Ok(20) + */ + map(fn: (value: T) => U): Effect; + + /** + * Chains another effect-producing function, allowing error propagation. + * + * @template U - The type of the next computation's success value. + * @template F - The type of the new error that may occur. + * @param {(value: T) => Effect} fn - Function returning a new Effect. + * @returns {Effect} - A new Effect propagating errors. + * + * @example + * const effect = Effect(() => 10).flatMap(x => Effect(() => x + 5)); + * console.log(effect.run()); // Ok(15) + */ + flatMap(fn: (value: T) => Effect): Effect; + + /** + * Runs the effect and returns a `Result`. + * + * @returns {Result} - A safe `Result`, encapsulating success (`Ok`) or failure (`Err`). + * + * @example + * const effect = Effect(() => { + * if (Math.random() > 0.5) throw new Error("Failure"); + * return 42; + * }); + * console.log(effect.run()); // Either Ok(42) or Err(Error) + */ + run(): Result; + + /** + * Recovers from errors by mapping them to a successful value. + * + * **Useful for handling failures gracefully.** + * + * @param {(error: E) => T} fn - Function to handle errors and return a fallback value. + * @returns {Effect} - A new `Effect` that ensures success. + * + * @example + * const effect = Effect(() => { throw new Error("Boom!"); }).recover(() => 0); + * console.log(effect.run()); // Ok(0) + */ + recover(fn: (error: E) => U): Effect; +}; + +/** + * Creates an `Effect` that represents a computation that may fail. + * This function **does not execute the computation immediately**, but defers execution. + * + * @template T - The type of the successful value. + * @template E - The type of the error. + * @param {() => T} fn - A function that produces a value (or throws an error). + * @returns {Effect} - A lazy effect that can be executed later. + * + * @example + * const safeDivide = Effect(() => 10 / 2); + * console.log(safeDivide.run()); // Ok(5) + */ +export function Effect(fn: () => T): Effect { + return { + /** + * Transforms the successful result of the effect. + * + * @template U - The transformed value type. + * @param {(value: T) => U} mapFn - Function to apply on success. + * @returns {Effect} - A new `Effect` with transformed data. + */ + map(mapFn: (value: T) => U): Effect { + return Effect(() => { + const result = fn(); + return mapFn(result); + }); + }, + + /** + * Chains another effect-producing function. + * + * **Useful for sequencing dependent computations.** + * + * @template U - The type of the next computation's success value. + * @template F - The new error type that may occur. + * @param {(value: T) => Effect} mapFn - Function returning a new Effect. + * @returns {Effect} - A new Effect propagating errors. + */ + flatMap(mapFn: (value: T) => Effect): Effect { + return Effect(() => { + const result = fn(); + return mapFn(result).run().unwrapOrThrow(); + }); + }, + + /** + * Executes the effect and returns a `Result`. + * + * @returns {Result} - A safe `Result`, encapsulating success (`Ok`) or failure (`Err`). + */ + run(): Result { + try { + return new Ok(fn()); + } catch (error) { + return new Err(error as E); + } + }, + + /** + * Recovers from errors by mapping them to a successful value. + * The resulting effect **ensures** success by removing the error type. + * + * @template U - The new success type after recovery. + * @param {(error: E) => U} recoveryFn - Function that handles errors and returns a fallback value. + * @returns {Effect} - An `Effect` that **always succeeds**. + */ + recover(recoveryFn: (error: E) => U): Effect { + return Effect(() => { + const result = this.run(); + return result.fold(recoveryFn, (value) => value); + }); + }, + }; +} diff --git a/src/functional/monads/EffectProperty-test.ts b/src/functional/monads/EffectProperty-test.ts new file mode 100644 index 0000000..8c18f5e --- /dev/null +++ b/src/functional/monads/EffectProperty-test.ts @@ -0,0 +1,93 @@ +import * as fc from 'fast-check'; +import { describe, expect, it } from 'vitest'; + +import { Effect } from './Effect.ts'; +import { Err, Ok } from './Result.ts'; + +describe('Effect (Property-Based Tests)', () => { + it('should satisfy identity law: effect.map(x => x) === effect', () => { + fc.assert( + fc.property(fc.anything(), (x) => { + const effect = Effect(() => x); + expect(effect.map((v) => v).run()).toStrictEqual(effect.run()); + }), + ); + }); + + it('should satisfy composition law: effect.map(f).map(g) === effect.map(x => g(f(x)))', () => { + fc.assert( + fc.property( + fc.anything(), + fc.func(fc.anything()), + fc.func(fc.anything()), + (x, f, g) => { + const effect = Effect(() => x); + const composedEffect = effect.map(f).map(g); + const directEffect = effect.map((v) => g(f(v))); + + expect(composedEffect.run()).toStrictEqual(directEffect.run()); + }, + ), + ); + }); + + it('should satisfy flatMap associativity: (effect.flatMap(f)).flatMap(g) === effect.flatMap(x => f(x).flatMap(g))', () => { + fc.assert( + fc.property( + fc.anything(), + fc.func( + fc.constantFrom( + Effect(() => 1), + Effect(() => 2), + ), + ), + fc.func( + fc.constantFrom( + Effect(() => 3), + Effect(() => 4), + ), + ), + (x, f, g) => { + const effect = Effect(() => x); + const left = effect.flatMap(f).flatMap(g); + const right = effect.flatMap((v) => f(v).flatMap(g)); + + expect(left.run()).toStrictEqual(right.run()); + }, + ), + ); + }); + + it('should always return Err if an effect throws', () => { + fc.assert( + fc.property(fc.string(), (errorMessage) => { + const effect = Effect(() => { + throw new Error(errorMessage); + }); + + expect(effect.run()).toStrictEqual(new Err(new Error(errorMessage))); + }), + ); + }); + + it('should recover from errors correctly', () => { + fc.assert( + fc.property(fc.string(), (errorMessage) => { + const effect = Effect(() => { + throw new Error(errorMessage); + }).recover(() => 'Recovered'); + + expect(effect.run()).toStrictEqual(new Ok('Recovered')); + }), + ); + }); + + it('should execute effects safely without throwing exceptions', () => { + fc.assert( + fc.property(fc.anything(), (x) => { + const effect = Effect(() => x); + expect(() => effect.run()).not.toThrow(); + }), + ); + }); +}); diff --git a/src/functional/monads/Option-test.ts b/src/functional/monads/Option-test.ts new file mode 100644 index 0000000..1a5126c --- /dev/null +++ b/src/functional/monads/Option-test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; + +import { None, Some } from './Option.ts'; + +describe('Option', () => { + describe('Some', () => { + it('should correctly identify as Some', () => { + const some = new Some(42); + expect(some.isSome()).toBe(true); + expect(some.isNone()).toBe(false); + }); + + it('should correctly map values', () => { + const some = new Some(10); + const result = some.map((x) => x * 2); + expect(result).toBeInstanceOf(Some); + expect(result.getOrElse(0)).toBe(20); + }); + + it('should correctly flatMap values', () => { + const some = new Some(10); + const result = some.flatMap((x) => new Some(x * 2)); + expect(result).toBeInstanceOf(Some); + expect(result.getOrElse(0)).toBe(20); + }); + + it('should return the contained value with getOrElse', () => { + const some = new Some('Hello'); + expect(some.getOrElse('Default')).toBe('Hello'); + }); + + it('should return itself in orElse', () => { + const some = new Some(99); + const alternative = new Some(100); + expect(some.orElse(alternative)).toBe(some); + }); + + it('should apply fold correctly', () => { + const some = new Some('Hello'); + const result = some.fold( + () => 'No value', + (value) => value.toUpperCase(), + ); + expect(result).toBe('HELLO'); + }); + + it('should correctly filter values', () => { + const some = new Some(10); + const filteredSome = some.filter((x) => x > 5); + expect(filteredSome).toBeInstanceOf(Some); + expect(filteredSome.getOrElse(0)).toBe(10); + + const filteredNone = some.filter((x) => x < 5); + expect(filteredNone).toBeInstanceOf(None); + }); + }); + + describe('None', () => { + it('should correctly identify as None', () => { + const none = None.instance; + expect(none.isSome()).toBe(false); + expect(none.isNone()).toBe(true); + }); + + it('should return None when mapped', () => { + const none = None.instance.map((x) => x * 2); + expect(none).toBeInstanceOf(None); + }); + + it('should return None when flatMapped', () => { + const none = None.instance.flatMap((x) => new Some(x * 2)); + expect(none).toBeInstanceOf(None); + }); + + it('should return default value with getOrElse', () => { + const none = None.instance; + expect(none.getOrElse('Default')).toBe('Default'); + }); + + it('should return alternative value in orElse', () => { + const none = None.instance; + const alternative = new Some(99); + expect(none.orElse(alternative)).toBe(alternative); + }); + + it('should apply fold correctly', () => { + const none = None.instance; + const result = none.fold( + () => 'No value', + (value) => (value as string).toUpperCase(), + ); + expect(result).toBe('No value'); + }); + + it('should always return None on filter', () => { + const none = None.instance; + const filteredNone = none.filter((x) => x > 5); + expect(filteredNone).toBeInstanceOf(None); + }); + }); +}); diff --git a/src/functional/monads/Option.ts b/src/functional/monads/Option.ts new file mode 100644 index 0000000..f848c2b --- /dev/null +++ b/src/functional/monads/Option.ts @@ -0,0 +1,284 @@ +/** + * Represents an optional value (`Some` for present values, `None` for absence). + * Implements a functional-style API for safe operations without null checks. + * + * @template T - The type of the contained value. + */ +export abstract class Option { + /** + * Creates an instance of `Some` if a value is present, otherwise returns `None`. + * + * @param {T | null | undefined} value - The value to wrap. + * @returns {Option} - `Some` if valid, otherwise `None`. + */ + static from(value: T | null | undefined): Option { + return value !== null && value !== undefined ? new Some(value) : None.instance; + } + + /** + * Checks if the `Option` contains a value. + * + * @returns {boolean} - `true` if `Some`, otherwise `false`. + */ + abstract isSome(): boolean; + + /** + * Checks if the `Option` is empty (`None`). + * + * @returns {boolean} - `true` if `None`, otherwise `false`. + */ + abstract isNone(): boolean; + + /** + * Transforms the contained value if present. + * + * @template U - The return type of the transformation function. + * @param {(value: T) => U} fn - A function to apply to the value if present. + * @returns {Option} - The transformed `Option`, or `None` if original was `None`. + */ + abstract map(fn: (value: T) => U): Option; + + /** + * Maps over the contained value but expects the function to return another `Option`. + * + * @template U - The return type wrapped in an `Option`. + * @param {(value: T) => Option} fn - A function returning an `Option`. + * @returns {Option} - The transformed `Option`, or `None` if original was `None`. + */ + abstract flatMap(fn: (value: T) => Option): Option; + + /** + * Provides a default value if the `Option` is `None`. + * + * @param {T} defaultValue - The fallback value. + * @returns {T} - The contained value if present, otherwise `defaultValue`. + */ + abstract getOrElse(defaultValue: T): T; + + /** + * Provides an alternative `Option` if the original is `None`. + * + * @param {Option} alternative - The alternative `Option` to return if this is `None`. + * @returns {Option} - The original if `Some`, otherwise `alternative`. + */ + abstract orElse(alternative: Option): Option; + + /** + * Executes a function based on whether the `Option` is `Some` or `None`. + * + * @template U - The return type. + * @param {() => U} ifNone - Function to call if `None`. + * @param {(value: T) => U} ifSome - Function to call with the value if `Some`. + * @returns {U} - The result of calling `ifNone` or `ifSome`. + */ + abstract fold(ifNone: () => U, ifSome: (value: T) => U): U; + + /** + * Filters the `Option` based on a predicate. + * + * @param {(value: T) => boolean} predicate - The condition to check. + * @returns {Option} - The original `Option` if the condition is met, otherwise `None`. + */ + abstract filter(predicate: (value: T) => boolean): Option; +} + +/** + * Represents a value that is present (`Some`). + * + * @template T - The type of the contained value. + */ +export class Some extends Option { + /** + * Creates an instance of `Some` with a valid value. + * + * @param {T} value - The contained value. + */ + constructor(private readonly value: T) { + super(); + } + + /** + * Checks if this is a `Some`, meaning it contains a valid value. + * + * @returns {boolean} - Always returns `true` for `Some`. + */ + isSome(): boolean { + return true; + } + + /** + * Checks if this is `None`, meaning no value is present. + * + * @returns {boolean} - Always returns `false` for `Some`. + */ + isNone(): boolean { + return false; + } + + /** + * Transforms the contained value using the provided function. + * + * @template U - The return type after transformation. + * @param {(value: T) => U} fn - The transformation function. + * @returns {Option} - A `Some` containing the transformed value. + */ + map(fn: (value: T) => U): Option { + return new Some(fn(this.value)); + } + + /** + * Transforms the contained value using a function that returns an `Option`. + * + * @template U - The return type wrapped in an `Option`. + * @param {(value: T) => Option} fn - The transformation function. + * @returns {Option} - The result of applying `fn`. + */ + flatMap(fn: (value: T) => Option): Option { + return fn(this.value); + } + + /** + * Retrieves the contained value, ignoring the provided default. + * + * @param {T} _defaultValue - (Unused) A fallback value. + * @returns {T} - The contained value. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getOrElse(_defaultValue: T): T { + return this.value; + } + + /** + * Returns the current `Some`, ignoring the provided alternative. + * + * @param {Option} _alternative - (Unused) An alternative `Option`. + * @returns {Option} - The same `Some`. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + orElse(_alternative: Option): Option { + return this; + } + + /** + * Executes the `ifSome` function with the contained value. + * + * @template U - The return type. + * @param {() => U} _ifNone - (Unused) Function to call if `None`. + * @param {(value: T) => U} ifSome - Function to call with the value if `Some`. + * @returns {U} - The result of calling `ifSome`. + */ + fold(_ifNone: () => U, ifSome: (value: T) => U): U { + return ifSome(this.value); + } + + /** + * Returns `Some` if the predicate is met, otherwise returns `None`. + * + * @param {(value: T) => boolean} predicate - A condition to check. + * @returns {Option} - `Some` if condition is met, otherwise `None`. + */ + filter(predicate: (value: T) => boolean): Option { + return predicate(this.value) ? this : None.instance; + } +} + +/** + * Represents an absence of value (`None`). + */ +export class None extends Option { + /** Singleton instance of `None` for efficiency. */ + static instance = new None(); + + /** Private constructor to enforce singleton pattern. */ + private constructor() { + super(); + } + + /** + * Checks if this is `Some`, meaning it contains a value. + * + * @returns {boolean} - Always returns `false` for `None`. + */ + isSome(): boolean { + return false; + } + + /** + * Checks if this is `None`, meaning no value is present. + * + * @returns {boolean} - Always returns `true` for `None`. + */ + isNone(): boolean { + return true; + } + + /** + * Maps over the value, but since `None` has no value, it always returns `None`. + * + * @template U - The return type. + * @returns {Option} - Always returns `None`. + * @param fn + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + map(fn: (value: never) => U): Option { + return this; + } + + /** + * Maps over the value expecting another `Option`, but since `None` has no value, it always returns `None`. + * + * @template U - The return type wrapped in an `Option`. + * @returns {Option} - Always returns `None`. + * @param fn + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + flatMap(fn: (value: never) => Option): Option { + return this; + } + + /** + * Returns the provided default value since `None` contains nothing. + * + * @template U - The return type. + * @param {U} defaultValue - The fallback value. + * @returns {U} - The provided default. + */ + getOrElse(defaultValue: U): U { + return defaultValue; + } + + /** + * Returns the provided alternative `Option` since `None` contains nothing. + * + * @template U - The return type. + * @param {Option} alternative - The alternative `Option` to return. + * @returns {Option} - The provided alternative. + */ + orElse(alternative: Option): Option { + return alternative; + } + + /** + * Calls `ifNone`, since `None` contains no value. + * + * @template U - The return type. + * @param {() => U} ifNone - Function to execute when `None`. + * @param {(value: never) => U} _ifSome - (Unused) Function for `Some`. + * @returns {U} - The result of calling `ifNone`. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fold(ifNone: () => U, _ifSome: (value: never) => U): U { + return ifNone(); + } + + /** + * Returns `None` since there is no value to filter. + * + * @param {(value: never) => boolean} _predicate - (Unused) A condition to check. + * @returns {Option} - Always returns `None`. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + filter(_predicate: (value: never) => boolean): Option { + return this; + } +} diff --git a/src/functional/monads/OptionProperty-test.ts b/src/functional/monads/OptionProperty-test.ts new file mode 100644 index 0000000..018730a --- /dev/null +++ b/src/functional/monads/OptionProperty-test.ts @@ -0,0 +1,106 @@ +import fc from 'fast-check'; +import { describe, expect, it } from 'vitest'; + +import { None, Some } from './Option.ts'; + +describe('Option (Property-Based Tests)', () => { + it('should satisfy map identity: map(x => x) === Option', () => { + fc.assert( + fc.property(fc.anything(), (x) => { + const option = new Some(x); + expect(option.map((y) => y)).toStrictEqual(option); + }), + ); + }); + + it('should satisfy associativity of map: map(f).map(g) === map(x => g(f(x)))', () => { + fc.assert( + fc.property( + fc.anything(), + fc.func(fc.anything()), + fc.func(fc.anything()), + (x, f, g) => { + const option = new Some(x); + const left = option.map(f).map(g); + const right = option.map((value) => g(f(value))); + + expect(left).toStrictEqual(right); + }, + ), + ); + }); + + it('should satisfy flatMap identity: flatMap(Some) === identity', () => { + fc.assert( + fc.property(fc.anything(), (x) => { + const option = new Some(x); + expect(option.flatMap((y) => new Some(y))).toStrictEqual(option); + }), + ); + }); + + it('should satisfy associativity of flatMap: flatMap(f).flatMap(g) === flatMap(x => f(x).flatMap(g))', () => { + fc.assert( + fc.property( + fc.anything(), + fc.func(fc.constant(new Some(42))), // Ensures `f` returns `Option` + fc.func(fc.constant(new Some(99))), // Ensures `g` returns `Option` + (x, f, g) => { + const option = new Some(x); + const left = option.flatMap(f).flatMap(g); + const right = option.flatMap((value) => f(value).flatMap(g)); + + expect(left).toStrictEqual(right); + }, + ), + ); + }); + + it('should ensure getOrElse returns value for Some and default for None', () => { + fc.assert( + fc.property(fc.anything(), fc.anything(), (x, defaultValue) => { + const some = new Some(x); + expect(some.getOrElse(defaultValue)).toBe(x); + + const none = None.instance; + expect(none.getOrElse(defaultValue)).toBe(defaultValue); + }), + ); + }); + + it('should return alternative value when orElse is applied to None', () => { + fc.assert( + fc.property(fc.anything(), (x) => { + const alternative = new Some(x); + expect(None.instance.orElse(alternative)).toStrictEqual(alternative); + }), + ); + }); + + it('should ensure filter(predicate) returns None for failing conditions', () => { + fc.assert( + fc.property(fc.anything(), fc.func(fc.boolean()), (x, predicate) => { + const option = new Some(x); + const filtered = option.filter(predicate); + + if (!predicate(x)) { + expect(filtered).toStrictEqual(None.instance); + } + }), + ); + }); + + it('should ensure None behaves consistently across all operations', () => { + fc.assert( + fc.property(fc.func(fc.anything()), fc.anything(), (fn, defaultValue) => { + const none = None.instance; + + expect(none.map(fn)).toStrictEqual(none); + expect(none.flatMap(() => new Some(42))).toStrictEqual(none); + expect(none.getOrElse(defaultValue)).toBe(defaultValue); + expect(none.orElse(new Some(42))).toStrictEqual(new Some(42)); + expect(none.filter(() => true)).toStrictEqual(none); + }), + ); + }); +}); diff --git a/src/functional/monads/Result-test.ts b/src/functional/monads/Result-test.ts new file mode 100644 index 0000000..66ec6d0 --- /dev/null +++ b/src/functional/monads/Result-test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest'; + +import { Err, Ok, Result } from './Result.ts'; + +describe('Result', () => { + describe('Ok', () => { + it('should be identified as Ok', () => { + const result = new Ok(42); + expect(result.isOk()).toBe(true); + expect(result.isErr()).toBe(false); + }); + + it('should map the value correctly', () => { + const result = new Ok(5).map((x) => x * 2); + expect(result.unwrapOr(-1)).toBe(10); + }); + + it('should flatMap correctly', () => { + const result = new Ok(3).flatMap((x) => new Ok(x + 1)); + expect(result.unwrapOr(-1)).toBe(4); + }); + + it('should return the contained value with unwrapOr', () => { + const result = new Ok(7); + expect(result.unwrapOr(0)).toBe(7); + }); + + it('should ignore alternative in orElse', () => { + const result = new Ok(100); + expect(result.orElse(new Err('Error')).unwrapOr(-1)).toBe(100); + }); + + it('should not modify on mapError', () => { + const result = new Ok(200).mapError(() => 'new error'); + expect(result.unwrapOr(-1)).toBe(200); + }); + + it('should execute fold correctly', () => { + const result = new Ok('Success').fold( + () => 'Failure', + (value) => `Processed: ${value}`, + ); + expect(result).toBe('Processed: Success'); + }); + }); + + describe('Err', () => { + it('should be identified as Err', () => { + const result = new Err('Something went wrong'); + expect(result.isOk()).toBe(false); + expect(result.isErr()).toBe(true); + }); + + it('should return default value on unwrapOr', () => { + const result = new Err('Error'); + expect(result.unwrapOr(99)).toBe(99); + }); + + it('should throw error on unwrapOrThrow', () => { + const result = new Err('Critical failure'); + expect(() => result.unwrapOrThrow()).toThrowError('Critical failure'); + }); + + it('should return alternative on orElse', () => { + const result = new Err('Error').orElse(new Ok(500)); + expect(result.unwrapOr(-1)).toBe(500); + }); + + it('should map the error correctly', () => { + const result = new Err('Oops').mapError((err) => `Fixed: ${err}`); + expect( + result.fold( + (err) => err, + () => 'Unexpected', + ), + ).toBe('Fixed: Oops'); + }); + + it('should return itself on map', () => { + const result = new Err('Nope').map((x) => (x as number) * 2); + expect(result.isErr()).toBe(true); + }); + + it('should return itself on flatMap', () => { + const result = new Err('Still nope').flatMap(() => new Ok(10)); + expect(result.isErr()).toBe(true); + }); + + it('should execute fold correctly', () => { + const result = new Err('Failure').fold( + (error) => `Handled: ${error}`, + () => 'Unexpected', + ); + expect(result).toBe('Handled: Failure'); + }); + }); + + describe('Chaining', () => { + it('should handle success case in chaining', () => { + const divide = (a: number, b: number): Result => + b === 0 ? new Err('Division by zero') : new Ok(a / b); + + const result = divide(10, 2) + .map((x) => x * 2) + .flatMap((x) => new Ok(x + 1)) + .unwrapOr(-1); + + expect(result).toBe(11); + }); + + it('should short-circuit on error in chaining', () => { + const divide = (a: number, b: number): Result => + b === 0 ? new Err('Division by zero') : new Ok(a / b); + + const result = divide(10, 0) + .map((x) => x * 2) + .flatMap((x) => new Ok(x + 1)) + .unwrapOr(-1); + + expect(result).toBe(-1); + }); + }); +}); diff --git a/src/functional/monads/Result.ts b/src/functional/monads/Result.ts new file mode 100644 index 0000000..e93943d --- /dev/null +++ b/src/functional/monads/Result.ts @@ -0,0 +1,328 @@ +/** + * Represents a result that can either be a success (`Ok`) or an error (`Err`). + * Provides functional methods for safe computations without exceptions. + * + * @template T - The type of the successful value. + * @template E - The type of the error. + */ +export abstract class Result { + /** + * Creates a success (`Ok`) result. + * + * @param {T} value - The value to wrap. + * @returns {Result} An `Ok` instance containing the value. + */ + static ok(value: T): Result { + return new Ok(value); + } + + /** + * Creates an error (`Err`) result. + * + * @param {E} error - The error to wrap. + * @returns {Result} An `Err` instance containing the error. + */ + static err(error: E): Result { + return new Err(error); + } + + /** + * Determines if the result is `Ok`. + * + * @returns {boolean} `true` if this is `Ok`, otherwise `false`. + */ + abstract isOk(): boolean; + + /** + * Determines if the result is `Err`. + * + * @returns {boolean} `true` if this is `Err`, otherwise `false`. + */ + abstract isErr(): boolean; + + /** + * Maps the successful value using the given function. + * + * @template U - The transformed success type. + * @param {(value: T) => U} fn - The function to apply. + * @returns {Result} A new `Result`. + */ + abstract map(fn: (value: T) => U): Result; + + /** + * Maps the error using the given function. + * + * @template F - The transformed error type. + * @param {(error: E) => F} fn - The function to apply. + * @returns {Result} A new `Result`. + */ + abstract mapError(fn: (error: E) => F): Result; + + /** + * Maps the successful value to another `Result`, allowing chaining. + * + * @template U - The transformed success type. + * @param {(value: T) => Result} fn - The function returning a `Result`. + * @returns {Result} A new `Result`. + */ + abstract flatMap(fn: (value: T) => Result): Result; + + /** + * Retrieves the success value if `Ok`, otherwise returns a default. + * + * @param {T} defaultValue - The default value if `Err`. + * @returns {T} The contained value or `defaultValue`. + */ + abstract unwrapOr(defaultValue: T): T; + + /** + * Retrieves the success value if `Ok`, otherwise throws an error. + * + * @returns {T} The contained value if `Ok`. + * @throws {Error} Throws an error if `Err`. + */ + abstract unwrapOrThrow(): T; + + /** + * Returns an alternative `Result` if this is `Err`. + * + * @param {Result} alternative - The alternative `Result`. + * @returns {Result} The original `Result` if `Ok`, otherwise `alternative`. + */ + abstract orElse(alternative: Result): Result; + + /** + * Folds the result into a single value by applying one of two functions. + * + * @template U - The return type. + * @param {(error: E) => U} ifErr - Function applied to `Err`. + * @param {(value: T) => U} ifOk - Function applied to `Ok`. + * @returns {U} The computed value. + */ + abstract fold(ifErr: (error: E) => U, ifOk: (value: T) => U): U; +} + +/** + * Represents a successful result (`Ok`), encapsulating a valid value. + * + * @template T - The type of the successful value. + */ +export class Ok extends Result { + /** + * Constructs an `Ok` instance with a successful value. + * + * @param {T} value - The valid value to wrap. + */ + constructor(private readonly value: T) { + super(); + } + + /** + * Checks if this is an `Ok`, meaning it contains a valid value. + * + * @returns {boolean} - Always returns `true` for `Ok`. + */ + isOk(): boolean { + return true; + } + + /** + * Checks if this is an `Err`, meaning it contains an error. + * + * @returns {boolean} - Always returns `false` for `Ok`. + */ + isErr(): boolean { + return false; + } + + /** + * Transforms the contained value using a function. + * + * @template U - The return type after transformation. + * @param {(value: T) => U} fn - The function to apply. + * @returns {Ok} - A new `Ok` containing the transformed value. + */ + map(fn: (value: T) => U): Result { + return new Ok(fn(this.value)); + } + + /** + * Maps the error, but since this is `Ok`, it remains unchanged. + * + * @template F - The new error type. + * @param {(_error: never) => F} _fn - Unused error mapping function. + * @returns {Ok} - Returns itself, as `Ok` has no error. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + mapError(_fn: (error: never) => F): Result { + return this; + } + + /** + * Maps the contained value to another `Result`, allowing chaining. + * + * @template U - The transformed success type. + * @param {(value: T) => Result} fn - The function returning a `Result`. + * @returns {Result} - The transformed `Result`. + */ + flatMap(fn: (value: T) => Result): Result { + return fn(this.value); + } + + /** + * Retrieves the contained value, ignoring the provided default. + * + * @param {T} _defaultValue - (Unused) A fallback value. + * @returns {T} - The contained value. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + unwrapOr(_defaultValue: T): T { + return this.value; + } + + /** + * Retrieves the contained value, or throws an error if it is `Err`. + * + * @returns {T} - The contained value. + * @throws {Error} Never throws, as this is always `Ok`. + */ + unwrapOrThrow(): T { + return this.value; + } + + /** + * Returns the current `Ok`, ignoring the provided alternative. + * + * @param {Result} _alternative - (Unused) An alternative `Result`. + * @returns {Ok} - Returns itself. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + orElse(_alternative: Result): Result { + return this; + } + + /** + * Executes the `ifOk` function with the contained value. + * + * @template U - The return type. + * @param {(error: never) => U} _ifErr - (Unused) Function for `Err`. + * @param {(value: T) => U} ifOk - Function to apply to the value. + * @returns {U} - The result of `ifOk(value)`. + */ + fold(_ifErr: (error: never) => U, ifOk: (value: T) => U): U { + return ifOk(this.value); + } +} + +/** + * Represents a failure result (`Err`), encapsulating an error. + * + * @template E - The type of the error. + */ +export class Err extends Result { + /** + * Constructs an `Err` instance with an error value. + * + * @param {E} error - The error to wrap. + */ + constructor(private readonly error: E) { + super(); + } + + /** + * Checks if this is an `Ok`, meaning it contains a valid value. + * + * @returns {boolean} - Always returns `false` for `Err`. + */ + isOk(): boolean { + return false; + } + + /** + * Checks if this is an `Err`, meaning it contains an error. + * + * @returns {boolean} - Always returns `true` for `Err`. + */ + isErr(): boolean { + return true; + } + + /** + * Maps the successful value, but since this is `Err`, it remains unchanged. + * + * @template U - The transformed success type. + * @param {(_value: never) => U} _fn - Unused transformation function. + * @returns {Err} - Returns itself. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + map(_fn: (value: never) => U): Result { + return this; + } + + /** + * Maps the error to another error type. + * + * @template F - The transformed error type. + * @param {(error: E) => F} fn - The function to transform the error. + * @returns {Err} - A new `Err` with the transformed error. + */ + mapError(fn: (error: E) => F): Result { + return new Err(fn(this.error)); + } + + /** + * Maps the contained value to another `Result`, allowing chaining. + * + * @template U - The transformed success type. + * @param {(_value: never) => Result} _fn - Unused transformation function. + * @returns {Err} - Returns itself. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + flatMap(_fn: (value: never) => Result): Result { + return this; + } + + /** + * Retrieves the provided default value since `Err` contains nothing. + * + * @template T - The success type. + * @param {T} defaultValue - The fallback value. + * @returns {T} - The provided default value. + */ + unwrapOr(defaultValue: T): T { + return defaultValue; + } + + /** + * Throws the contained error. + * + * @throws {Error} Throws the contained error. + */ + unwrapOrThrow(): never { + throw new Error(String(this.error)); + } + + /** + * Returns the provided alternative `Result` since `Err` contains nothing. + * + * @template T - The success type. + * @param {Result} alternative - The alternative `Result`. + * @returns {Result} - The provided alternative. + */ + orElse(alternative: Result): Result { + return alternative; + } + + /** + * Executes the `ifErr` function with the contained error. + * + * @template U - The return type. + * @param {(error: E) => U} ifErr - Function applied to the error. + * @param {(_value: never) => U} _ifOk - (Unused) Function for `Ok`. + * @returns {U} - The result of `ifErr(error)`. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + fold(ifErr: (error: E) => U, _ifOk: (value: never) => U): U { + return ifErr(this.error); + } +} diff --git a/src/functional/monads/ResultProperty-test.ts b/src/functional/monads/ResultProperty-test.ts new file mode 100644 index 0000000..55fac14 --- /dev/null +++ b/src/functional/monads/ResultProperty-test.ts @@ -0,0 +1,94 @@ +import * as fc from 'fast-check'; +import { describe, expect, it } from 'vitest'; + +import { Err, Ok, Result } from './Result.ts'; + +describe('Result (Property-based Tests)', () => { + it('should preserve identity under map', () => { + fc.assert( + fc.property(fc.integer(), (x) => { + expect(new Ok(x).map((v) => v)).toStrictEqual(new Ok(x)); + }), + ); + }); + + it('should preserve identity under flatMap', () => { + fc.assert( + fc.property(fc.integer(), (x) => { + expect(new Ok(x).flatMap((v) => new Ok(v))).toStrictEqual(new Ok(x)); + }), + ); + }); + + it('should not change Err values under map', () => { + fc.assert( + fc.property(fc.string(), fc.integer(), (error, x) => { + expect(new Err(error).map(() => x)).toStrictEqual(new Err(error)); + }), + ); + }); + + it('should not change Ok values under mapError', () => { + fc.assert( + fc.property(fc.integer(), fc.string(), (x, error) => { + expect(new Ok(x).mapError(() => error)).toStrictEqual(new Ok(x)); + }), + ); + }); + + it('unwrapOr should return the default value for Err', () => { + fc.assert( + fc.property(fc.string(), fc.integer(), (error, defaultValue) => { + expect(new Err(error).unwrapOr(defaultValue)).toBe(defaultValue); + }), + ); + }); + + it('should satisfy function composition associativity under map', () => { + fc.assert( + fc.property(fc.integer(), (x) => { + const f = (v: number) => v + 1; + const g = (v: number) => v * 2; + expect(new Ok(x).map(f).map(g)).toStrictEqual(new Ok(g(f(x)))); + }), + ); + }); + + it('should correctly apply flatMap chaining', () => { + fc.assert( + fc.property(fc.integer(), (x) => { + const double = (v: number) => new Ok(v * 2); + expect(new Ok(x).flatMap(double)).toStrictEqual(new Ok(x * 2)); + }), + ); + }); + + it('fold should always resolve to a value', () => { + fc.assert( + fc.property(fc.integer(), fc.string(), (x, error) => { + expect( + new Ok(x).fold( + () => 'Error', + (v) => String(v), + ), + ).toBe(String(x)); // Ensure consistent return type + expect( + new Err(error).fold( + () => 'Error', + (v) => v, + ), + ).toBe('Error'); + }), + ); + }); + + it('should respect Err propagation in flatMap', () => { + fc.assert( + fc.property(fc.string(), (error) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const failingFn = (_: number): Result => new Err(error); + expect(new Ok(42).flatMap(failingFn)).toStrictEqual(new Err(error)); + }), + ); + }); +}); diff --git a/src/functional/monads/index.ts b/src/functional/monads/index.ts new file mode 100644 index 0000000..93d4646 --- /dev/null +++ b/src/functional/monads/index.ts @@ -0,0 +1,3 @@ +export { Effect } from './Effect.ts'; +export { Result, Err, Ok } from './Result.ts'; +export { Option, Some, None } from './Option.ts'; diff --git a/src/functional/partial/Partial-test.ts b/src/functional/partial/Partial-test.ts new file mode 100644 index 0000000..dbee128 --- /dev/null +++ b/src/functional/partial/Partial-test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest'; + +import { partial, partialRight } from './Partial.ts'; + +describe('partial', () => { + it('should partially apply arguments from the left', () => { + const multiply = (a: number, b: number, c: number) => a * b * c; + const multiplyBy2 = partial(multiply, 2); + + expect(multiplyBy2(3, 4)).toBe(24); + expect(multiplyBy2(5, 6)).toBe(60); + }); + + it('should work with single fixed arguments', () => { + const greet = (greeting: string, name: string) => `${greeting}, ${name}!`; + const sayHello = partial(greet, 'Hello'); + + expect(sayHello('Alice')).toBe('Hello, Alice!'); + expect(sayHello('Bob')).toBe('Hello, Bob!'); + }); + + it('should handle functions with multiple fixed arguments', () => { + const joinStrings = (a: string, b: string, c: string) => `${a}-${b}-${c}`; + const joinAB = partial(joinStrings, 'A', 'B'); + + expect(joinAB('C')).toBe('A-B-C'); + }); + + it('should correctly handle functions with different arities', () => { + // eslint-disable-next-line max-params + const add = (a: number, b: number, c: number, d: number) => a + b + c + d; + const addOneAndTwo = partial(add, 1, 2); + + expect(addOneAndTwo(3, 4)).toBe(10); + expect(addOneAndTwo(5, 6)).toBe(14); + }); +}); + +describe('partialRight', () => { + it('should partially apply arguments from the right', () => { + const formatDate = (year: number, month: number, day: number) => `${year}-${month}-${day}`; + const formatThisYear = partialRight(formatDate, 2024); + + expect(formatThisYear(5, 12)).toBe('5-12-2024'); + }); + + it('should work with single fixed arguments', () => { + const greet = (name: string, punctuation: string) => `Hello, ${name}${punctuation}`; + const excitedGreeting = partialRight(greet, '!'); + + expect(excitedGreeting('Alice')).toBe('Hello, Alice!'); + expect(excitedGreeting('Bob')).toBe('Hello, Bob!'); + }); + + it('should handle functions with multiple fixed arguments', () => { + const wrapText = (prefix: string, text: string, suffix: string) => + `${prefix}${text}${suffix}`; + const wrapWithStars = partialRight(wrapText, '*', '*'); + + expect(wrapWithStars('Hello')).toBe('Hello**'); + expect(wrapWithStars('World')).toBe('World**'); + }); +}); diff --git a/src/functional/partial/Partial.ts b/src/functional/partial/Partial.ts new file mode 100644 index 0000000..304ee65 --- /dev/null +++ b/src/functional/partial/Partial.ts @@ -0,0 +1,43 @@ +/** + * Partially applies arguments to a function, fixing some parameters from the left. + * + * @template T - The tuple type of the fixed arguments. + * @template U - The tuple type of the remaining arguments. + * @template R - The return type of the function. + * @param {(...args: [...T, ...U]) => R} fn - The function to partially apply. + * @param {...T} presetArgs - The arguments to fix from the left. + * @returns {(...remainingArgs: U) => R} - A new function that takes only the remaining arguments. + * + * @example + * const multiply = (a: number, b: number, c: number) => a * b * c; + * const multiplyBy2 = partial(multiply, 2); + * console.log(multiplyBy2(3, 4)); // Output: 24 + */ +export function partial( + fn: (...args: [...T, ...U]) => R, + ...presetArgs: T +): (...remainingArgs: U) => R { + return (...remainingArgs: U) => fn(...presetArgs, ...remainingArgs); +} + +/** + * Partially applies arguments to a function, fixing some parameters from the right. + * + * @template T - The tuple type of the remaining arguments. + * @template U - The tuple type of the fixed arguments. + * @template R - The return type of the function. + * @param {(...args: [...T, ...U]) => R} fn - The function to partially apply. + * @param {...U} presetArgs - The arguments to fix from the right. + * @returns {(...remainingArgs: T) => R} - A new function that takes only the remaining arguments. + * + * @example + * const formatDate = (year: number, month: number, day: number) => `${year}-${month}-${day}`; + * const formatThisYear = partialRight(formatDate, 2024); + * console.log(formatThisYear(5, 12)); // Output: "5-12-2024" + */ +export function partialRight( + fn: (...args: [...T, ...U]) => R, + ...presetArgs: U +): (...remainingArgs: T) => R { + return (...remainingArgs: T) => fn(...remainingArgs, ...presetArgs); +} diff --git a/src/functional/partial/PartialProperty-test.ts b/src/functional/partial/PartialProperty-test.ts new file mode 100644 index 0000000..0cd5dd9 --- /dev/null +++ b/src/functional/partial/PartialProperty-test.ts @@ -0,0 +1,74 @@ +import * as fc from 'fast-check'; +import { describe, expect, it } from 'vitest'; + +import { partial, partialRight } from './Partial.ts'; + +describe('partial (Property-based Tests)', () => { + it('should behave as expected when partially applying arguments from the left', () => { + fc.assert( + fc.property( + fc.func<[number, number], number>(fc.integer()), // Generates (a, b) => number + fc.integer(), + fc.integer(), + (fn, x, y) => { + const partiallyApplied = partial(fn, x); + expect(partiallyApplied(y)).toBe(fn(x, y)); // Expected behavior + }, + ), + ); + }); + + it('should satisfy partial(partial(fn, x), y) === partial(fn, x, y)', () => { + fc.assert( + fc.property( + fc.func<[number, number, number], number>(fc.integer()), // Generates (a, b, c) => number + fc.integer(), + fc.integer(), + fc.integer(), + // eslint-disable-next-line max-params + (fn, x, y, z) => { + const partiallyApplied1 = partial(partial(fn, x), y); + const partiallyApplied2 = partial(fn, x, y); + + // Instead of comparing functions, compare outputs + expect(partiallyApplied1(z)).toBe(partiallyApplied2(z)); + }, + ), + ); + }); +}); + +describe('partialRight (Property-based Tests)', () => { + it('should behave as expected when partially applying arguments from the right', () => { + fc.assert( + fc.property( + fc.func<[number, number], number>(fc.integer()), // Generates (a, b) => number + fc.integer(), + fc.integer(), + (fn, x, y) => { + const partiallyApplied = partialRight(fn, y); + expect(partiallyApplied(x)).toBe(fn(x, y)); // Expected behavior + }, + ), + ); + }); + + it('should satisfy partialRight(partialRight(fn, y), x) === partialRight(fn, x, y)', () => { + fc.assert( + fc.property( + fc.func<[number, number, number], number>(fc.integer()), // Generates (a, b, c) => number + fc.integer(), + fc.integer(), + fc.integer(), + // eslint-disable-next-line max-params + (fn, x, y, z) => { + const partiallyApplied1 = partialRight(partialRight(fn, y), x); + const partiallyApplied2 = partialRight(fn, x, y); + + // Instead of comparing functions, compare outputs + expect(partiallyApplied1(z)).toBe(partiallyApplied2(z)); + }, + ), + ); + }); +}); diff --git a/src/index.ts b/src/index.ts index 79bb0f1..8daffc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './algorithms/index.ts'; export * from './data-structures/index.ts'; +export * from './functional/index.ts';