@nlozgachev/pipekit 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -0
- package/esm/mod.js +3 -0
- package/esm/package.json +3 -0
- package/esm/src/Composition/compose.js +3 -0
- package/esm/src/Composition/curry.js +42 -0
- package/esm/src/Composition/flip.js +20 -0
- package/esm/src/Composition/flow.js +8 -0
- package/esm/src/Composition/fn.js +85 -0
- package/esm/src/Composition/index.js +10 -0
- package/esm/src/Composition/memoize.js +66 -0
- package/esm/src/Composition/not.js +25 -0
- package/esm/src/Composition/pipe.js +3 -0
- package/esm/src/Composition/tap.js +33 -0
- package/esm/src/Composition/uncurry.js +32 -0
- package/esm/src/Core/Arr.js +461 -0
- package/esm/src/Core/InternalTypes.js +1 -0
- package/esm/src/Core/Option.js +195 -0
- package/esm/src/Core/Rec.js +167 -0
- package/esm/src/Core/RemoteData.js +210 -0
- package/esm/src/Core/Result.js +173 -0
- package/esm/src/Core/Task.js +108 -0
- package/esm/src/Core/TaskResult.js +63 -0
- package/esm/src/Core/Validation.js +215 -0
- package/esm/src/Core/index.js +8 -0
- package/esm/src/Types/NonEmptyList.js +14 -0
- package/esm/src/Types/index.js +1 -0
- package/package.json +60 -0
- package/script/mod.js +19 -0
- package/script/package.json +3 -0
- package/script/src/Composition/compose.js +6 -0
- package/script/src/Composition/curry.js +48 -0
- package/script/src/Composition/flip.js +24 -0
- package/script/src/Composition/flow.js +11 -0
- package/script/src/Composition/fn.js +98 -0
- package/script/src/Composition/index.js +26 -0
- package/script/src/Composition/memoize.js +71 -0
- package/script/src/Composition/not.js +29 -0
- package/script/src/Composition/pipe.js +6 -0
- package/script/src/Composition/tap.js +37 -0
- package/script/src/Composition/uncurry.js +38 -0
- package/script/src/Core/Arr.js +464 -0
- package/script/src/Core/InternalTypes.js +2 -0
- package/script/src/Core/Option.js +198 -0
- package/script/src/Core/Rec.js +170 -0
- package/script/src/Core/RemoteData.js +213 -0
- package/script/src/Core/Result.js +176 -0
- package/script/src/Core/Task.js +111 -0
- package/script/src/Core/TaskResult.js +66 -0
- package/script/src/Core/Validation.js +218 -0
- package/script/src/Core/index.js +24 -0
- package/script/src/Types/NonEmptyList.js +18 -0
- package/script/src/Types/index.js +17 -0
- package/types/mod.d.ts +4 -0
- package/types/mod.d.ts.map +1 -0
- package/types/src/Composition/compose.d.ts +33 -0
- package/types/src/Composition/compose.d.ts.map +1 -0
- package/types/src/Composition/curry.d.ts +43 -0
- package/types/src/Composition/curry.d.ts.map +1 -0
- package/types/src/Composition/flip.d.ts +21 -0
- package/types/src/Composition/flip.d.ts.map +1 -0
- package/types/src/Composition/flow.d.ts +56 -0
- package/types/src/Composition/flow.d.ts.map +1 -0
- package/types/src/Composition/fn.d.ts +76 -0
- package/types/src/Composition/fn.d.ts.map +1 -0
- package/types/src/Composition/index.d.ts +11 -0
- package/types/src/Composition/index.d.ts.map +1 -0
- package/types/src/Composition/memoize.d.ts +46 -0
- package/types/src/Composition/memoize.d.ts.map +1 -0
- package/types/src/Composition/not.d.ts +26 -0
- package/types/src/Composition/not.d.ts.map +1 -0
- package/types/src/Composition/pipe.d.ts +56 -0
- package/types/src/Composition/pipe.d.ts.map +1 -0
- package/types/src/Composition/tap.d.ts +31 -0
- package/types/src/Composition/tap.d.ts.map +1 -0
- package/types/src/Composition/uncurry.d.ts +54 -0
- package/types/src/Composition/uncurry.d.ts.map +1 -0
- package/types/src/Core/Arr.d.ts +355 -0
- package/types/src/Core/Arr.d.ts.map +1 -0
- package/types/src/Core/InternalTypes.d.ts +14 -0
- package/types/src/Core/InternalTypes.d.ts.map +1 -0
- package/types/src/Core/Option.d.ts +214 -0
- package/types/src/Core/Option.d.ts.map +1 -0
- package/types/src/Core/Rec.d.ts +121 -0
- package/types/src/Core/Rec.d.ts.map +1 -0
- package/types/src/Core/RemoteData.d.ts +196 -0
- package/types/src/Core/RemoteData.d.ts.map +1 -0
- package/types/src/Core/Result.d.ts +185 -0
- package/types/src/Core/Result.d.ts.map +1 -0
- package/types/src/Core/Task.d.ts +125 -0
- package/types/src/Core/Task.d.ts.map +1 -0
- package/types/src/Core/TaskResult.d.ts +78 -0
- package/types/src/Core/TaskResult.d.ts.map +1 -0
- package/types/src/Core/Validation.d.ts +217 -0
- package/types/src/Core/Validation.d.ts.map +1 -0
- package/types/src/Core/index.d.ts +9 -0
- package/types/src/Core/index.d.ts.map +1 -0
- package/types/src/Types/NonEmptyList.d.ts +29 -0
- package/types/src/Types/NonEmptyList.d.ts.map +1 -0
- package/types/src/Types/index.d.ts +2 -0
- package/types/src/Types/index.d.ts.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# @nlozgachev/pipekit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@nlozgachev/pipekit)[](https://jsr.io/@nlozgachev/pipekit)[](https://www.typescriptlang.org)[](https://deno.com)
|
|
4
|
+
|
|
5
|
+
A functional programming toolkit for TypeScript that speaks your language.
|
|
6
|
+
|
|
7
|
+
## What is this?
|
|
8
|
+
|
|
9
|
+
A collection of types and utilities that help you write safer, composition-centric, more predictable TypeScript. If you've ever dealt with `null` checks scattered across your codebase, nested `try/catch` blocks, or `{ data: T | null; loading: boolean; error: Error | null }` — this library gives you better tools for all of that.
|
|
10
|
+
|
|
11
|
+
## Do I need to know functional programming to use this?
|
|
12
|
+
|
|
13
|
+
No. The library avoids FP-specific jargon wherever possible. You won't find `Monad`, `Functor`, or `Applicative` in the API. Instead, you'll work with names that describe what they do:
|
|
14
|
+
|
|
15
|
+
- `Option` — a value that might not exist
|
|
16
|
+
- `Result` — an operation that can succeed or fail
|
|
17
|
+
- `map` — transform a value inside a container
|
|
18
|
+
- `chain` — sequence operations that might fail
|
|
19
|
+
- `match` — handle each case explicitly
|
|
20
|
+
|
|
21
|
+
You can start using these right away and learn the underlying concepts as you go.
|
|
22
|
+
|
|
23
|
+
## What's included?
|
|
24
|
+
|
|
25
|
+
### pipekit/Core
|
|
26
|
+
|
|
27
|
+
Each of these is both a TypeScript type and a module of functions for working with values of that type — constructors, transformations, and ways to extract a value back out.
|
|
28
|
+
|
|
29
|
+
- **`Option<A>`** — a value that might not exist. Replaces `T | null | undefined`. Key operations: `fromNullable`, `map`, `chain`, `filter`, `match`, `getOrElse`, `recover`.
|
|
30
|
+
- **`Result<E, A>`** — an operation that succeeds with `A` or fails with `E`. Replaces `try/catch`. Key operations: `tryCatch`, `map`, `mapError`, `chain`, `match`, `getOrElse`, `recover`.
|
|
31
|
+
- **`Validation<E, A>`** — like `Result`, but accumulates **all** errors instead of stopping at the first. Built for form validation. Key operations: `combine`, `combineAll`, `ap`, `map`, `match`.
|
|
32
|
+
- **`Task<A>`** — a lazy async operation that doesn't run until called. Key operations: `from`, `map`, `chain`, `all`, `delay`.
|
|
33
|
+
- **`TaskResult<E, A>`** — a lazy async operation that can fail. `Task` + `Result` combined. Key operations: `tryCatch`, `map`, `mapError`, `chain`, `match`, `recover`.
|
|
34
|
+
- **`RemoteData<E, A>`** — models the four states of a data fetch: `NotAsked`, `Loading`, `Failure`, `Success`. Replaces `{ data, loading, error }` flag soup. Key operations: `notAsked`, `loading`, `failure`, `success`, `map`, `match`, `toResult`.
|
|
35
|
+
- **`Arr`** — array operations that return `Option` instead of throwing or returning `undefined`. Operations: `head`, `last`, `findFirst`, `findLast`, `partition`, `groupBy`, `zip`, `traverse`, and more.
|
|
36
|
+
- **`Rec`** — record/object operations. Operations: `lookup`, `map`, `filter`, `pick`, `omit`, `merge`, and more.
|
|
37
|
+
|
|
38
|
+
### pipekit/Types
|
|
39
|
+
|
|
40
|
+
- **`NonEmptyList<A>`** — an array guaranteed to have at least one element.
|
|
41
|
+
|
|
42
|
+
### pipekit/Composition
|
|
43
|
+
|
|
44
|
+
- **`pipe`** — pass a value through a series of functions, left to right.
|
|
45
|
+
- **`flow`** — compose functions into a reusable pipeline (like `pipe` without an initial value).
|
|
46
|
+
- **`compose`** — compose functions right to left (traditional mathematical composition).
|
|
47
|
+
- **`curry` / `uncurry`** — convert between multi-argument and single-argument functions.
|
|
48
|
+
- **`tap`** — run a side effect (like logging) without breaking the pipeline.
|
|
49
|
+
- **`memoize`** — cache function results.
|
|
50
|
+
- **`identity`**, **`constant`**, **`not`**, **`and`**, **`or`**, **`once`**, **`flip`** — common function utilities.
|
|
51
|
+
|
|
52
|
+
## What does "composition-centric" mean?
|
|
53
|
+
|
|
54
|
+
Everything in the library is designed to work with `pipe` — a function that passes a value through a series of transformations, top to bottom. The alternative is nesting calls inside each other, which reads inside-out:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { Option } from "@nlozgachev/pipekit/Core";
|
|
58
|
+
import { pipe } from "@nlozgachev/pipekit/Composition";
|
|
59
|
+
|
|
60
|
+
// Without pipe: execution order is the reverse of reading order
|
|
61
|
+
const userName = Option.getOrElse(
|
|
62
|
+
Option.map(
|
|
63
|
+
Option.fromNullable(users.get("123")),
|
|
64
|
+
u => u.name
|
|
65
|
+
),
|
|
66
|
+
"Anonymous"
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// With pipe: reads top to bottom, matches execution order
|
|
70
|
+
const userName = pipe(
|
|
71
|
+
users.get("123"), // User | undefined
|
|
72
|
+
Option.fromNullable, // Option<User>
|
|
73
|
+
Option.map(u => u.name), // Option<string>
|
|
74
|
+
Option.getOrElse("Anonymous") // string
|
|
75
|
+
);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
No method chaining, no class hierarchies. Just functions that connect together.
|
|
79
|
+
|
|
80
|
+
## What does "data-last" mean and why should I care?
|
|
81
|
+
|
|
82
|
+
Every operation takes the data it operates on as the **last** argument. This means you can partially apply them — get a function back without providing data yet — which makes `flow` work naturally.
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { Option } from "@nlozgachev/pipekit/Core";
|
|
86
|
+
import { flow } from "@nlozgachev/pipekit/Composition";
|
|
87
|
+
|
|
88
|
+
// Data-first: can't partially apply, so you're stuck writing wrapper functions
|
|
89
|
+
function formatName(user: User | null): string {
|
|
90
|
+
const opt = Option.fromNullable(user);
|
|
91
|
+
const name = Option.map(opt, u => u.name);
|
|
92
|
+
return Option.getOrElse(name, "Anonymous");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
users.map(u => formatName(u)); // needs an arrow function wrapper
|
|
96
|
+
|
|
97
|
+
// Data-last: operations are curried, so flow connects them directly
|
|
98
|
+
const formatName = flow(
|
|
99
|
+
Option.fromNullable<User>,
|
|
100
|
+
Option.map(u => u.name),
|
|
101
|
+
Option.getOrElse("Anonymous")
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
users.map(formatName); // no wrapper — it's already a function of User
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## How does this help me write safer code?
|
|
108
|
+
|
|
109
|
+
The core types make invalid states unrepresentable. Instead of stacking null checks and hoping you handled every branch, the shape of the code reflects the shape of the data:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// Before: null handling that doesn't scale
|
|
113
|
+
function getDisplayCity(userId: string): string {
|
|
114
|
+
const user = getUser(userId);
|
|
115
|
+
if (user === null) return "UNKNOWN";
|
|
116
|
+
if (user.address === null) return "UNKNOWN";
|
|
117
|
+
if (user.address.city === null) return "UNKNOWN";
|
|
118
|
+
return user.address.city.toUpperCase();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// After: flat, readable, same guarantees
|
|
122
|
+
function getDisplayCity(userId: string): string {
|
|
123
|
+
return pipe(
|
|
124
|
+
getUser(userId), // User | null
|
|
125
|
+
Option.fromNullable, // Option<User>
|
|
126
|
+
Option.chain(u => Option.fromNullable(u.address)), // Option<Address>
|
|
127
|
+
Option.chain(a => Option.fromNullable(a.city)), // Option<string>
|
|
128
|
+
Option.map(c => c.toUpperCase()), // Option<string>
|
|
129
|
+
Option.getOrElse("UNKNOWN") // string
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The same idea applies to error handling with `Result`, form validation with `Validation`, async operations with `Task` and `TaskResult`, and loading states with `RemoteData`.
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
## How do I install it?
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
# Deno
|
|
142
|
+
deno add jsr:@nlozgachev/pipekit
|
|
143
|
+
|
|
144
|
+
# npm / pnpm / yarn / bun
|
|
145
|
+
npm add @nlozgachev/pipekit
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## How do I get started?
|
|
149
|
+
|
|
150
|
+
Start with `pipe` and `Option`. These two cover the most common pain point — dealing with values that might not exist:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { Option } from "@nlozgachev/pipekit/Core";
|
|
154
|
+
import { pipe } from "@nlozgachev/pipekit/Composition";
|
|
155
|
+
|
|
156
|
+
// Read a user's preferred language from their settings, fall back to the app default
|
|
157
|
+
const language = pipe(
|
|
158
|
+
userSettings.get(userId), // UserSettings | undefined
|
|
159
|
+
Option.fromNullable, // Option<UserSettings>
|
|
160
|
+
Option.map(s => s.language), // Option<string>
|
|
161
|
+
Option.getOrElse(DEFAULT_LANGUAGE) // string
|
|
162
|
+
);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Once that feels natural, reach for `Result` when operations can fail with a meaningful error — parsing, network calls, database lookups:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { Result } from "@nlozgachev/pipekit/Core";
|
|
169
|
+
|
|
170
|
+
// Parse user input and look up the record — both steps can fail
|
|
171
|
+
const record = pipe(
|
|
172
|
+
parseId(rawInput), // Result<ParseError, number>
|
|
173
|
+
Result.chain(id => db.find(id)), // Result<ParseError | NotFoundError, Record>
|
|
174
|
+
Result.map(r => r.name), // Result<ParseError | NotFoundError, string>
|
|
175
|
+
);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
And `Validation` when you need to collect multiple errors at once, like form validation.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
package/esm/mod.js
ADDED
package/esm/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a multi-argument function into a curried function.
|
|
3
|
+
* The inverse of `uncurry`.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const add = (a: number, b: number) => a + b;
|
|
8
|
+
* const curriedAdd = curry(add);
|
|
9
|
+
* curriedAdd(1)(2); // 3
|
|
10
|
+
*
|
|
11
|
+
* // Partial application
|
|
12
|
+
* const addTen = curriedAdd(10);
|
|
13
|
+
* addTen(5); // 15
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* @see {@link uncurry} for the inverse operation
|
|
17
|
+
* @see {@link curry3} for 3-argument functions
|
|
18
|
+
* @see {@link curry4} for 4-argument functions
|
|
19
|
+
*/
|
|
20
|
+
export const curry = (f) => (a) => (b) => f(a, b);
|
|
21
|
+
/**
|
|
22
|
+
* Converts a 3-argument function into a curried function.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const add3 = (a: number, b: number, c: number) => a + b + c;
|
|
27
|
+
* const curriedAdd3 = curry3(add3);
|
|
28
|
+
* curriedAdd3(1)(2)(3); // 6
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const curry3 = (f) => (a) => (b) => (c) => f(a, b, c);
|
|
32
|
+
/**
|
|
33
|
+
* Converts a 4-argument function into a curried function.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const add4 = (a: number, b: number, c: number, d: number) => a + b + c + d;
|
|
38
|
+
* const curriedAdd4 = curry4(add4);
|
|
39
|
+
* curriedAdd4(1)(2)(3)(4); // 10
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export const curry4 = (f) => (a) => (b) => (c) => (d) => f(a, b, c, d);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flips the order of arguments for a curried binary function.
|
|
3
|
+
* Converts a data-last function to data-first.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Original data-last (for pipe)
|
|
8
|
+
* pipe(
|
|
9
|
+
* Option.of(5),
|
|
10
|
+
* Option.map(n => n * 2)
|
|
11
|
+
* ); // Some(10)
|
|
12
|
+
*
|
|
13
|
+
* // Flipped to data-first
|
|
14
|
+
* const mapFirst = flip(Option.map);
|
|
15
|
+
* mapFirst(Option.of(5))(n => n * 2); // Some(10)
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @see {@link uncurry} for converting curried functions to multi-argument functions
|
|
19
|
+
*/
|
|
20
|
+
export const flip = (f) => (b) => (a) => f(a)(b);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the value unchanged. The identity function.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* identity(42); // 42
|
|
7
|
+
* pipe(Option.of(5), Option.fold(() => 0, identity)); // 5
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
export const identity = (a) => a;
|
|
11
|
+
/**
|
|
12
|
+
* Creates a function that always returns the given value, ignoring its argument.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const always42 = constant(42);
|
|
17
|
+
* always42(); // 42
|
|
18
|
+
* [1, 2, 3].map(constant("x")); // ["x", "x", "x"]
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const constant = (a) => () => a;
|
|
22
|
+
/** Always returns `true`. */
|
|
23
|
+
export const constTrue = () => true;
|
|
24
|
+
/** Always returns `false`. */
|
|
25
|
+
export const constFalse = () => false;
|
|
26
|
+
/** Always returns `null`. */
|
|
27
|
+
export const constNull = () => null;
|
|
28
|
+
/** Always returns `undefined`. */
|
|
29
|
+
export const constUndefined = () => undefined;
|
|
30
|
+
/** Always returns `void`. */
|
|
31
|
+
export const constVoid = () => { };
|
|
32
|
+
/**
|
|
33
|
+
* Combines two predicates with logical AND.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* const isPositive = (n: number) => n > 0;
|
|
38
|
+
* const isEven = (n: number) => n % 2 === 0;
|
|
39
|
+
* const isPositiveEven = and(isPositive, isEven);
|
|
40
|
+
*
|
|
41
|
+
* isPositiveEven(4); // true
|
|
42
|
+
* isPositiveEven(-2); // false
|
|
43
|
+
* isPositiveEven(3); // false
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export const and = (p1, p2) => (...args) => p1(...args) && p2(...args);
|
|
47
|
+
/**
|
|
48
|
+
* Combines two predicates with logical OR.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* const isNegative = (n: number) => n < 0;
|
|
53
|
+
* const isZero = (n: number) => n === 0;
|
|
54
|
+
* const isNonPositive = or(isNegative, isZero);
|
|
55
|
+
*
|
|
56
|
+
* isNonPositive(-1); // true
|
|
57
|
+
* isNonPositive(0); // true
|
|
58
|
+
* isNonPositive(1); // false
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export const or = (p1, p2) => (...args) => p1(...args) || p2(...args);
|
|
62
|
+
/**
|
|
63
|
+
* Creates a function that executes at most once.
|
|
64
|
+
* Subsequent calls return the cached result from the first execution.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* let count = 0;
|
|
69
|
+
* const initOnce = once(() => { count++; return "initialized"; });
|
|
70
|
+
*
|
|
71
|
+
* initOnce(); // "initialized", count === 1
|
|
72
|
+
* initOnce(); // "initialized", count === 1 (not called again)
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export const once = (f) => {
|
|
76
|
+
let called = false;
|
|
77
|
+
let result;
|
|
78
|
+
return () => {
|
|
79
|
+
if (!called) {
|
|
80
|
+
result = f();
|
|
81
|
+
called = true;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./compose.js";
|
|
2
|
+
export * from "./curry.js";
|
|
3
|
+
export * from "./flip.js";
|
|
4
|
+
export * from "./fn.js";
|
|
5
|
+
export * from "./flow.js";
|
|
6
|
+
export * from "./memoize.js";
|
|
7
|
+
export * from "./not.js";
|
|
8
|
+
export * from "./pipe.js";
|
|
9
|
+
export * from "./tap.js";
|
|
10
|
+
export * from "./uncurry.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a memoized version of a function that caches results.
|
|
3
|
+
* Subsequent calls with the same argument return the cached result.
|
|
4
|
+
*
|
|
5
|
+
* By default, uses the argument directly as the cache key.
|
|
6
|
+
* For complex arguments, provide a custom `keyFn` to generate cache keys.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* // Basic usage
|
|
11
|
+
* const expensive = memoize((n: number) => {
|
|
12
|
+
* console.log("Computing...");
|
|
13
|
+
* return n * 2;
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* expensive(5); // logs "Computing...", returns 10
|
|
17
|
+
* expensive(5); // returns 10 (cached, no log)
|
|
18
|
+
* expensive(3); // logs "Computing...", returns 6
|
|
19
|
+
*
|
|
20
|
+
* // With custom key function for objects
|
|
21
|
+
* const fetchUser = memoize(
|
|
22
|
+
* (opts: { id: string }) => fetch(`/users/${opts.id}`),
|
|
23
|
+
* opts => opts.id
|
|
24
|
+
* );
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const memoize = (f, keyFn = (a) => a) => {
|
|
28
|
+
const cache = new Map();
|
|
29
|
+
return (a) => {
|
|
30
|
+
const key = keyFn(a);
|
|
31
|
+
if (cache.has(key)) {
|
|
32
|
+
return cache.get(key);
|
|
33
|
+
}
|
|
34
|
+
const result = f(a);
|
|
35
|
+
cache.set(key, result);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Creates a memoized version of a function using WeakMap.
|
|
41
|
+
* Only works with object arguments, but allows garbage collection
|
|
42
|
+
* of cached values when keys are no longer referenced.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const processUser = memoizeWeak((user: User) => {
|
|
47
|
+
* return expensiveOperation(user);
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* const user = { id: 1, name: "Alice" };
|
|
51
|
+
* processUser(user); // computed
|
|
52
|
+
* processUser(user); // cached
|
|
53
|
+
* // When `user` is garbage collected, cached result is too
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export const memoizeWeak = (f) => {
|
|
57
|
+
const cache = new WeakMap();
|
|
58
|
+
return (a) => {
|
|
59
|
+
if (cache.has(a)) {
|
|
60
|
+
return cache.get(a);
|
|
61
|
+
}
|
|
62
|
+
const result = f(a);
|
|
63
|
+
cache.set(a, result);
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Negates a predicate function.
|
|
3
|
+
* Returns a new predicate that returns true when the original returns false, and vice versa.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const isEven = (n: number) => n % 2 === 0;
|
|
8
|
+
* const isOdd = not(isEven);
|
|
9
|
+
*
|
|
10
|
+
* isOdd(3); // true
|
|
11
|
+
* isOdd(4); // false
|
|
12
|
+
*
|
|
13
|
+
* // With array methods
|
|
14
|
+
* const numbers = [1, 2, 3, 4, 5];
|
|
15
|
+
* numbers.filter(not(isEven)); // [1, 3, 5]
|
|
16
|
+
*
|
|
17
|
+
* // In pipelines
|
|
18
|
+
* pipe(
|
|
19
|
+
* users,
|
|
20
|
+
* Array.filter(not(isAdmin)),
|
|
21
|
+
* Array.map(u => u.name)
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export const not = (predicate) => (...args) => !predicate(...args);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes a side effect function and returns the original value unchanged.
|
|
3
|
+
* Useful for logging, debugging, or other side effects within a pipeline.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* // Debugging a pipeline
|
|
8
|
+
* pipe(
|
|
9
|
+
* Option.of(5),
|
|
10
|
+
* tap(x => console.log("Before map:", x)),
|
|
11
|
+
* Option.map(n => n * 2),
|
|
12
|
+
* tap(x => console.log("After map:", x)),
|
|
13
|
+
* Option.getOrElse(0)
|
|
14
|
+
* );
|
|
15
|
+
* // logs: "Before map: { kind: 'Some', value: 5 }"
|
|
16
|
+
* // logs: "After map: { kind: 'Some', value: 10 }"
|
|
17
|
+
* // returns: 10
|
|
18
|
+
*
|
|
19
|
+
* // Collecting intermediate values
|
|
20
|
+
* const values: number[] = [];
|
|
21
|
+
* pipe(
|
|
22
|
+
* [1, 2, 3],
|
|
23
|
+
* arr => arr.map(n => n * 2),
|
|
24
|
+
* tap(arr => values.push(...arr))
|
|
25
|
+
* );
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @see {@link Option.tap} for Option-specific tap that only runs on Some
|
|
29
|
+
*/
|
|
30
|
+
export const tap = (f) => (a) => {
|
|
31
|
+
f(a);
|
|
32
|
+
return a;
|
|
33
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// deno-lint-ignore no-explicit-any
|
|
2
|
+
export function uncurry(f) {
|
|
3
|
+
// f.length determines the outer arity; inner.length determines the inner arity.
|
|
4
|
+
// The typed overloads guarantee these are 0, 1, or 2 total args.
|
|
5
|
+
// deno-lint-ignore no-explicit-any
|
|
6
|
+
return (...args) => {
|
|
7
|
+
const inner = f(...args.slice(0, f.length));
|
|
8
|
+
return inner.length === 0 ? inner() : inner(...args.slice(f.length));
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Converts a curried 3-argument function into a multi-argument function.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* const curriedAdd3 = (a: number) => (b: number) => (c: number) => a + b + c;
|
|
17
|
+
* const add3 = uncurry3(curriedAdd3);
|
|
18
|
+
* add3(1, 2, 3); // 6
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const uncurry3 = (f) => (a, b, c) => f(a)(b)(c);
|
|
22
|
+
/**
|
|
23
|
+
* Converts a curried 4-argument function into a multi-argument function.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const curriedAdd4 = (a: number) => (b: number) => (c: number) => (d: number) => a + b + c + d;
|
|
28
|
+
* const add4 = uncurry4(curriedAdd4);
|
|
29
|
+
* add4(1, 2, 3, 4); // 10
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export const uncurry4 = (f) => (a, b, c, d) => f(a)(b)(c)(d);
|