-
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 #3 from helabenkhalfallah/feature/functional-patterns
[FEATURE]: add functional programming patterns
- Loading branch information
Showing
19 changed files
with
1,239 additions
and
5 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
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,59 @@ | ||
import { describe, expect, it } from 'vitest'; | ||
|
||
import { match } from './Match.ts'; | ||
|
||
describe('match function', () => { | ||
it('should return the correct result for number matching', () => { | ||
const result = match(5, [ | ||
[(n) => n === 10, () => 'Exactly ten'], | ||
[(n) => n === 0, () => 'Zero'], | ||
[(n) => n > 10, (n) => `Greater than 10: ${n}`], | ||
[(n) => n > 0, (n) => `Positive: ${n}`], | ||
]); | ||
|
||
expect(result).toBe('Positive: 5'); | ||
}); | ||
|
||
it('should return the correct result for string matching', () => { | ||
const result = match('hello', [ | ||
[(s) => s === 'world', () => 'Matched world'], | ||
[(s) => s === 'hello', () => 'Matched hello'], | ||
]); | ||
|
||
expect(result).toBe('Matched hello'); | ||
}); | ||
|
||
it('should return the correct result for object matching', () => { | ||
const user = { name: 'Alice', age: 25 }; | ||
|
||
const result = match(user, [ | ||
[(u) => u.age > 30, () => 'Older than 30'], | ||
[(u) => u.age >= 25, () => 'Adult'], | ||
]); | ||
|
||
expect(result).toBe('Adult'); | ||
}); | ||
|
||
it('should return the correct result for tuple matching', () => { | ||
const point: [number, number] = [3, 4]; | ||
|
||
const result = match(point, [ | ||
[(p) => p[0] === 0 && p[1] === 0, () => 'Origin'], | ||
[(p) => p[0] === 0, () => 'On Y-axis'], | ||
[(p) => p[1] === 0, () => 'On X-axis'], | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
[(_) => true, () => 'Somewhere else'], | ||
]); | ||
|
||
expect(result).toBe('Somewhere else'); | ||
}); | ||
|
||
it('should throw an error if no match is found', () => { | ||
expect(() => | ||
match(100, [ | ||
[(n) => n === 10, () => 'Exactly ten'], | ||
[(n) => n === 0, () => 'Zero'], | ||
]), | ||
).toThrowError('No match found'); | ||
}); | ||
}); |
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,44 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
/** | ||
* Pattern matching function for TypeScript. | ||
* | ||
* @template T - The type of the value to be matched. | ||
* @param {T} value - The input value to match against patterns. | ||
* @param {Array<[ (value: T) => boolean, (value: T) => any ]>} patterns - | ||
* An array of tuples where: | ||
* - The first element is a predicate function that checks if the pattern applies. | ||
* - The second element is a handler function that executes when the pattern matches. | ||
* @returns {any} - The result of the first matching handler function. | ||
* @throws {Error} If no pattern matches the input value. | ||
* | ||
* @example | ||
* const result = match(5, [ | ||
* [(n) => n === 10, () => "Exactly ten"], | ||
* [(n) => n > 0, (n) => `Positive: ${n}`], | ||
* ]); | ||
* console.log(result); // Output: "Positive: 5" | ||
* | ||
* @example | ||
* // Matching against an Option type | ||
* type None = { type: "None" }; | ||
* type Some<T> = { type: "Some"; value: T }; | ||
* type Option<T> = Some<T> | None; | ||
* | ||
* const None: None = { type: "None" }; | ||
* const Some = <T>(value: T): Some<T> => ({ type: "Some", value }); | ||
* | ||
* const value: Option<number> = Some(15); | ||
* const message = match(value, [ | ||
* [(x) => x.type === "Some", (x) => `Some value: ${x.value}`], | ||
* [(x) => x.type === "None", () => "No value"] | ||
* ]); | ||
* console.log(message); // Output: "Some value: 15" | ||
*/ | ||
export function match<T>(value: T, patterns: [(value: T) => boolean, (value: T) => any][]): any { | ||
for (const [predicate, handler] of patterns) { | ||
if (predicate(value)) { | ||
return handler(value); | ||
} | ||
} | ||
throw new Error('No match found'); | ||
} |
100 changes: 100 additions & 0 deletions
100
src/functional/algebraic-data-type/MatchProperty-test.ts
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,100 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
import * as fc from 'fast-check'; | ||
import { describe, expect, it } from 'vitest'; | ||
|
||
import { match } from './Match.ts'; | ||
|
||
describe('match function - property-based testing', () => { | ||
it('should return the expected result for positive numbers', () => { | ||
fc.assert( | ||
fc.property(fc.integer({ min: 1 }), (n) => { | ||
const result = match(n, [[(x) => x > 0, (x) => `Positive: ${x}`]]); | ||
expect(result).toBe(`Positive: ${n}`); | ||
}), | ||
); | ||
}); | ||
|
||
it('should correctly match string patterns', () => { | ||
fc.assert( | ||
fc.property(fc.string(), (s) => { | ||
const result = match(s, [ | ||
[(x) => x.startsWith('A'), () => 'Starts with A'], | ||
[(x) => x.endsWith('Z'), () => 'Ends with Z'], | ||
[(x) => x.length === 0, () => 'Empty string'], | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
[(_) => true, () => 'Fallback'], | ||
]); | ||
|
||
// Property: The result should be a known output | ||
expect(['Starts with A', 'Ends with Z', 'Empty string', 'Fallback']).toContain( | ||
result, | ||
); | ||
}), | ||
); | ||
}); | ||
|
||
it('should correctly match objects', () => { | ||
fc.assert( | ||
fc.property( | ||
fc.record({ | ||
age: fc.integer({ min: 0, max: 120 }), | ||
name: fc.string(), | ||
}), | ||
(user) => { | ||
const result = match(user, [ | ||
[(u) => u.age < 18, () => 'Minor'], | ||
[(u) => u.age >= 18, () => 'Adult'], | ||
]); | ||
|
||
expect(['Minor', 'Adult']).toContain(result); | ||
}, | ||
), | ||
); | ||
}); | ||
|
||
it('should always return a value from the match cases', () => { | ||
fc.assert( | ||
fc.property(fc.anything(), (randomValue) => { | ||
try { | ||
match(randomValue, [ | ||
[(x) => typeof x === 'number', (x) => `Number: ${x}`], | ||
[(x) => typeof x === 'string', (x) => `String: ${x}`], | ||
]); | ||
} catch (e) { | ||
expect(e).toBeInstanceOf(Error); | ||
} | ||
}), | ||
); | ||
}); | ||
|
||
it('should throw an error when no match is found', () => { | ||
fc.assert( | ||
fc.property(fc.anything(), (randomValue) => { | ||
expect(() => | ||
match(randomValue, [ | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
[(x) => false, () => 'This never matches'], // Always false, so match() should always fail | ||
]), | ||
).toThrowError('No match found'); | ||
}), | ||
); | ||
}); | ||
|
||
it('should throw an error when no match is found', () => { | ||
fc.assert( | ||
fc.property(fc.anything(), (randomValue) => { | ||
const patterns: [(value: any) => boolean, (value: any) => any][] = [ | ||
[(x) => typeof x === 'number', () => "It's a number"], | ||
[(x) => typeof x === 'string', () => "It's a string"], | ||
]; | ||
|
||
// Check if any predicate matches | ||
const hasMatch = patterns.some(([predicate]) => predicate(randomValue)); | ||
|
||
if (!hasMatch) { | ||
expect(() => match(randomValue, patterns)).toThrowError('No match found'); | ||
} | ||
}), | ||
); | ||
}); | ||
}); |
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
Oops, something went wrong.