fp-pack 0.9.2 → 0.9.3
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/dist/ai-addons/fp-pack-agent-addon.md +1 -1
- package/dist/skills/fp-pack/SKILL.md +214 -218
- package/dist/skills/fp-pack.md +214 -218
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# fp-pack AI Agent Skills
|
|
9
9
|
|
|
10
|
-
Document Version: 0.9.
|
|
10
|
+
Document Version: 0.9.3
|
|
11
11
|
|
|
12
12
|
## ⚠️ Activation Condition (Read First)
|
|
13
13
|
|
|
@@ -18,7 +18,7 @@ Before following this document:
|
|
|
18
18
|
- Check `node_modules/fp-pack` exists
|
|
19
19
|
- Check existing code imports from `fp-pack` or `fp-pack/stream`
|
|
20
20
|
|
|
21
|
-
If `fp-pack` is **not** installed, use the project
|
|
21
|
+
If `fp-pack` is **not** installed, use the project's existing conventions. Do **not** suggest adding fp-pack unless the user asks.
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -30,16 +30,159 @@ When writing or editing code in an fp-pack project:
|
|
|
30
30
|
- Use `stream/*` for lazy/large/iterable processing; use array/object utils for small/eager data.
|
|
31
31
|
- Use SideEffect-aware pipes only when you need **early exit**: `pipeSideEffect*` / `pipeAsyncSideEffect*`.
|
|
32
32
|
- **Never call `runPipeResult` inside a pipeline**; call it at the boundary (event handler, service wrapper, etc.).
|
|
33
|
-
- If TypeScript inference gets stuck with a data-last generic, use a small wrapper
|
|
33
|
+
- If TypeScript inference gets stuck with a data-last generic, use a small wrapper or `pipeHint`.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## ⛔ CRITICAL MISTAKES TO AVOID (Read This First!)
|
|
38
|
+
|
|
39
|
+
Before you write any code, know these anti-patterns. These are the most common mistakes that break fp-pack pipelines:
|
|
40
|
+
|
|
41
|
+
### ❌ WRONG: Using `() => value` for constants in pipelines
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// ❌ BAD - Type inference fails
|
|
45
|
+
const broken = pipe(
|
|
46
|
+
() => [1, 2, 3, 4, 5],
|
|
47
|
+
filter((n: number) => n % 2 === 0) // Error!
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// ✅ CORRECT - Use from() for constant values
|
|
51
|
+
const works = pipe(
|
|
52
|
+
from([1, 2, 3, 4, 5]),
|
|
53
|
+
filter((n: number) => n % 2 === 0)
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### ❌ WRONG: Using raw values with ifElse/cond
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
// ❌ BAD
|
|
61
|
+
const broken = ifElse((score: number) => score >= 60, 'pass', 'fail');
|
|
62
|
+
|
|
63
|
+
// ✅ CORRECT - Use from() to wrap constant values
|
|
64
|
+
const works = ifElse((score: number) => score >= 60, from('pass'), from('fail'));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### ❌ WRONG: Calling runPipeResult inside a pipeline
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// ❌ BAD
|
|
71
|
+
const broken = pipe(validateUser, runPipeResult, processUser);
|
|
72
|
+
|
|
73
|
+
// ✅ CORRECT - Call runPipeResult OUTSIDE the pipeline
|
|
74
|
+
const pipeline = pipeSideEffect(validateUser, processUser);
|
|
75
|
+
const result = runPipeResult(pipeline(userData));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### ❌ WRONG: Using pipe with SideEffect-returning functions
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// ❌ BAD
|
|
82
|
+
const broken = pipe(
|
|
83
|
+
findUser, // Returns User | SideEffect
|
|
84
|
+
(user) => user.email // Error! SideEffect has no 'email'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// ✅ CORRECT - Use pipeSideEffect for SideEffect handling
|
|
88
|
+
const works = pipeSideEffect(
|
|
89
|
+
findUser,
|
|
90
|
+
(user) => user.email // Automatically skipped if SideEffect
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### ❌ WRONG: Mutating arrays/objects
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// ❌ BAD
|
|
98
|
+
users.push(newUser);
|
|
99
|
+
user.name = 'Updated';
|
|
100
|
+
|
|
101
|
+
// ✅ CORRECT - Use immutable operations
|
|
102
|
+
const updatedUsers = append(newUser, users);
|
|
103
|
+
const updatedUser = assoc('name', 'Updated', user);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### ❌ WRONG: Imperative loops instead of declarative transforms
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// ❌ BAD
|
|
110
|
+
const results = [];
|
|
111
|
+
for (const user of users) {
|
|
112
|
+
if (user.active) results.push(user.name.toUpperCase());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ✅ CORRECT
|
|
116
|
+
const results = pipe(
|
|
117
|
+
filter((user: User) => user.active),
|
|
118
|
+
map(user => user.name.toUpperCase())
|
|
119
|
+
)(users);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Remember:** If you encounter any of these patterns, STOP and use the correct fp-pack approach instead!
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 💡 Real-World Examples (Learn by Doing)
|
|
127
|
+
|
|
128
|
+
Study these common patterns first:
|
|
129
|
+
|
|
130
|
+
### Example 1: User Data Processing Pipeline
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { pipe, filter, map, take, sortBy } from 'fp-pack';
|
|
134
|
+
|
|
135
|
+
const getTopActiveUsers = pipe(
|
|
136
|
+
filter((user: User) => user.active && user.lastLogin > cutoffDate),
|
|
137
|
+
sortBy((user) => -user.activityScore),
|
|
138
|
+
map(user => ({ id: user.id, name: user.name, score: user.activityScore })),
|
|
139
|
+
take(10)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const topUsers = getTopActiveUsers(allUsers);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Example 2: API Request with Error Handling
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { pipeAsyncSideEffect, SideEffect, runPipeResult } from 'fp-pack';
|
|
149
|
+
|
|
150
|
+
const fetchUserData = pipeAsyncSideEffect(
|
|
151
|
+
async (userId: string) => {
|
|
152
|
+
const res = await fetch(`/api/users/${userId}`);
|
|
153
|
+
return res.ok ? res : SideEffect.of(() => `HTTP ${res.status}`);
|
|
154
|
+
},
|
|
155
|
+
async (res) => res.json(),
|
|
156
|
+
(data) => data.verified ? data : SideEffect.of(() => 'User not verified')
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Call at boundary
|
|
160
|
+
const result = runPipeResult(await fetchUserData('user-123'));
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Example 3: Data Transformation with from()
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { pipe, from, ifElse, filter, map } from 'fp-pack';
|
|
167
|
+
|
|
168
|
+
// Constant value injection
|
|
169
|
+
const getStatusMessage = ifElse(
|
|
170
|
+
(count: number) => count > 0,
|
|
171
|
+
from('Items available'),
|
|
172
|
+
from('No items found')
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Data-first pattern
|
|
176
|
+
const processWithData = pipe(
|
|
177
|
+
from([1, 2, 3, 4, 5]),
|
|
178
|
+
filter((n: number) => n % 2 === 0),
|
|
179
|
+
map(n => n * 2)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const result = processWithData(); // [4, 8]
|
|
183
|
+
```
|
|
36
184
|
|
|
37
|
-
|
|
38
|
-
1) Function composition via `pipe` / `pipeAsync`
|
|
39
|
-
2) Declarative, immutable transforms (avoid loops and mutations)
|
|
40
|
-
3) Practical TypeScript inference (data-last + currying for most helpers)
|
|
41
|
-
4) Optional early-exit handling via `SideEffect` (only when needed)
|
|
42
|
-
5) Lazy iterable processing via `fp-pack/stream` for performance-sensitive flows
|
|
185
|
+
**These patterns cover 90% of real-world use cases!**
|
|
43
186
|
|
|
44
187
|
---
|
|
45
188
|
|
|
@@ -47,8 +190,6 @@ fp-pack is a TypeScript FP utility library focused on:
|
|
|
47
190
|
|
|
48
191
|
### `pipe` (sync)
|
|
49
192
|
|
|
50
|
-
Use `pipe` to build a unary function:
|
|
51
|
-
|
|
52
193
|
```ts
|
|
53
194
|
import { pipe, filter, map, take } from 'fp-pack';
|
|
54
195
|
|
|
@@ -61,8 +202,6 @@ const processUsers = pipe(
|
|
|
61
202
|
|
|
62
203
|
### `pipeAsync` (async)
|
|
63
204
|
|
|
64
|
-
Use `pipeAsync` when any step is async:
|
|
65
|
-
|
|
66
205
|
```ts
|
|
67
206
|
import { pipeAsync } from 'fp-pack';
|
|
68
207
|
|
|
@@ -75,120 +214,82 @@ const fetchUser = pipeAsync(
|
|
|
75
214
|
|
|
76
215
|
---
|
|
77
216
|
|
|
78
|
-
## Currying & Data-Last
|
|
79
|
-
|
|
80
|
-
Most multi-arg helpers are **data-last** and **curried**, so they work naturally in `pipe`:
|
|
217
|
+
## Currying & Data-Last
|
|
81
218
|
|
|
219
|
+
Most multi-arg helpers are **data-last** and **curried**:
|
|
82
220
|
- Good: `map(fn)`, `filter(pred)`, `replace(from, to)`, `assoc('k', v)`, `path(['a','b'])`
|
|
83
|
-
-
|
|
84
|
-
|
|
85
|
-
Single-arg helpers are already unary; “currying them” adds no value—just use them directly.
|
|
221
|
+
- Single-arg helpers are already unary—just use them directly
|
|
86
222
|
|
|
87
223
|
---
|
|
88
224
|
|
|
89
|
-
## TypeScript: Data-last Generic Inference
|
|
225
|
+
## TypeScript: Data-last Generic Inference
|
|
90
226
|
|
|
91
|
-
Some data-last helpers return a **generic function** whose type is only determined by the final data argument.
|
|
92
|
-
Inside `pipe`/`pipeAsync`, TypeScript sometimes can’t infer that type, so you may need a tiny hint.
|
|
227
|
+
Some data-last helpers return a **generic function** whose type is only determined by the final data argument. Inside `pipe`, TypeScript sometimes can't infer that type.
|
|
93
228
|
|
|
94
|
-
###
|
|
229
|
+
### Quick fixes (choose 1)
|
|
95
230
|
|
|
96
231
|
```ts
|
|
97
232
|
import { pipe, pipeHint, zip, some } from 'fp-pack';
|
|
98
233
|
|
|
99
|
-
// 1) data-first wrapper
|
|
234
|
+
// 1) data-first wrapper
|
|
100
235
|
const withWrapper = pipe(
|
|
101
236
|
(values: number[]) => zip([1, 2, 3], values),
|
|
102
237
|
some(([a, b]) => a > b)
|
|
103
238
|
);
|
|
104
239
|
|
|
105
|
-
// 2)
|
|
106
|
-
const withHint = pipe(
|
|
107
|
-
zip([1, 2, 3]) as (values: number[]) => Array<[number, number]>,
|
|
108
|
-
some(([a, b]) => a > b)
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
// 3) pipeHint helper (keeps pipeline style)
|
|
240
|
+
// 2) pipeHint helper
|
|
112
241
|
const withPipeHint = pipe(
|
|
113
242
|
pipeHint<number[], Array<[number, number]>>(zip([1, 2, 3])),
|
|
114
243
|
some(([a, b]) => a > b)
|
|
115
244
|
);
|
|
116
245
|
```
|
|
117
246
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
- Array: `chunk`, `drop`, `take`, `zip`
|
|
121
|
-
- Object: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
|
|
122
|
-
- Async: `timeout`
|
|
123
|
-
- Stream: `chunk`, `drop`, `take`, `zip`
|
|
124
|
-
|
|
125
|
-
When in doubt: check the `.d.ts` signature (see “Quick Signature Lookup”).
|
|
247
|
+
**Utilities that may need a hint:** `chunk`, `drop`, `take`, `zip`, `assoc`, `path`, `prop`, `timeout`
|
|
126
248
|
|
|
127
249
|
---
|
|
128
250
|
|
|
129
251
|
## SideEffect Pattern (Use Only When Needed)
|
|
130
252
|
|
|
131
|
-
Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you
|
|
253
|
+
Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you need **early termination**:
|
|
132
254
|
- validation pipelines that should stop early
|
|
133
255
|
- recoverable errors you want to model as data
|
|
134
256
|
- branching flows where you want to short-circuit
|
|
135
257
|
|
|
136
258
|
### SideEffect-aware pipes
|
|
137
259
|
- `pipeSideEffect` / `pipeAsyncSideEffect`: convenient, but may widen effects to `any`
|
|
138
|
-
- `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended
|
|
139
|
-
|
|
140
|
-
### Rules
|
|
141
|
-
- Do **not** call `runPipeResult` or `matchSideEffect` **inside** the pipeline.
|
|
142
|
-
- Prefer `isSideEffect` for precise runtime narrowing in both branches.
|
|
143
|
-
- Use `runPipeResult` only at the boundary, and provide generics if inference is widened.
|
|
260
|
+
- `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended)
|
|
144
261
|
|
|
145
262
|
### Key functions
|
|
146
263
|
- `SideEffect.of(effectFn, label?)`
|
|
147
264
|
- `isSideEffect(value)` (type guard)
|
|
148
265
|
- `runPipeResult(result)` (execute effect or return value; **outside** pipelines)
|
|
149
|
-
- `matchSideEffect(result, { value, effect })`
|
|
150
|
-
|
|
151
|
-
### `runPipeResult` type behavior (important)
|
|
152
266
|
|
|
153
|
-
|
|
154
|
-
- If the input is **widened** to `SideEffect<any>` or `any` (common with non-strict pipelines), `runPipeResult(x)` becomes `any` unless you provide generics.
|
|
267
|
+
### Example
|
|
155
268
|
|
|
156
269
|
```ts
|
|
157
|
-
import {
|
|
158
|
-
pipeSideEffect,
|
|
159
|
-
pipeSideEffectStrict,
|
|
160
|
-
SideEffect,
|
|
161
|
-
isSideEffect,
|
|
162
|
-
runPipeResult,
|
|
163
|
-
} from 'fp-pack';
|
|
164
|
-
|
|
165
|
-
const nonStrict = pipeSideEffect(
|
|
166
|
-
(n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
|
|
167
|
-
);
|
|
168
|
-
const widened: number | SideEffect<any> = nonStrict(-1);
|
|
169
|
-
const unsafe = runPipeResult(widened); // any
|
|
270
|
+
import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
|
|
170
271
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
272
|
+
const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
|
|
273
|
+
const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
|
|
274
|
+
|
|
275
|
+
const result = pipeline(-1); // number | SideEffect<'NEG'>
|
|
175
276
|
|
|
176
|
-
if (isSideEffect(
|
|
177
|
-
const err = runPipeResult(
|
|
277
|
+
if (isSideEffect(result)) {
|
|
278
|
+
const err = runPipeResult(result); // 'NEG'
|
|
279
|
+
} else {
|
|
280
|
+
// result is number
|
|
178
281
|
}
|
|
179
282
|
```
|
|
180
283
|
|
|
181
284
|
---
|
|
182
285
|
|
|
183
|
-
## Stream Functions (`fp-pack/stream`)
|
|
286
|
+
## Stream Functions (`fp-pack/stream`)
|
|
184
287
|
|
|
185
288
|
Use stream utilities when:
|
|
186
|
-
- data is large or unbounded
|
|
187
|
-
- you want lazy evaluation
|
|
289
|
+
- data is large or unbounded
|
|
290
|
+
- you want lazy evaluation
|
|
188
291
|
- you want to support `Iterable` and `AsyncIterable`
|
|
189
292
|
|
|
190
|
-
Stream utilities are in `fp-pack/stream` and are designed to be pipe-friendly.
|
|
191
|
-
|
|
192
293
|
```ts
|
|
193
294
|
import { pipe } from 'fp-pack';
|
|
194
295
|
import { range, filter, map, take, toArray } from 'fp-pack/stream';
|
|
@@ -205,115 +306,78 @@ const result = first100SquaresOfEvens(range(Infinity));
|
|
|
205
306
|
|
|
206
307
|
---
|
|
207
308
|
|
|
208
|
-
## Available Functions (
|
|
209
|
-
|
|
210
|
-
This is not a complete API reference; it’s the “what to reach for” index when composing pipelines.
|
|
309
|
+
## Available Functions (Quick Index)
|
|
211
310
|
|
|
212
311
|
### Composition
|
|
213
312
|
- `pipe`, `pipeAsync`
|
|
214
313
|
- SideEffect-aware: `pipeSideEffect`, `pipeSideEffectStrict`, `pipeAsyncSideEffect`, `pipeAsyncSideEffectStrict`
|
|
215
|
-
- Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `
|
|
314
|
+
- Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `curry`, `compose`
|
|
216
315
|
- SideEffect helpers: `SideEffect`, `isSideEffect`, `matchSideEffect`, `runPipeResult`
|
|
217
316
|
|
|
218
317
|
### Array
|
|
219
|
-
-
|
|
318
|
+
- Transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
|
|
220
319
|
- Queries: `find`, `some`, `every`
|
|
221
|
-
- Slicing: `take`, `drop`, `
|
|
222
|
-
- Ordering
|
|
223
|
-
-
|
|
224
|
-
- Combining: `concat`, `append`, `prepend`, `flatten`
|
|
320
|
+
- Slicing: `take`, `drop`, `chunk`
|
|
321
|
+
- Ordering: `sort`, `sortBy`, `groupBy`, `uniqBy`
|
|
322
|
+
- Combining: `zip`, `concat`, `append`, `flatten`
|
|
225
323
|
|
|
226
324
|
### Object
|
|
227
|
-
- Access: `prop`, `
|
|
325
|
+
- Access: `prop`, `path`, `propOr`, `pathOr`
|
|
228
326
|
- Pick/drop: `pick`, `omit`
|
|
229
327
|
- Updates: `assoc`, `assocPath`, `dissocPath`
|
|
230
328
|
- Merge: `merge`, `mergeDeep`
|
|
231
329
|
- Transforms: `mapValues`, `evolve`
|
|
232
|
-
- Predicates: `has`
|
|
233
330
|
|
|
234
331
|
### Control Flow
|
|
235
332
|
- `ifElse`, `when`, `unless`, `cond`, `guard`, `tryCatch`
|
|
236
333
|
|
|
237
334
|
### Async
|
|
238
335
|
- `retry`, `timeout`, `delay`
|
|
239
|
-
-
|
|
336
|
+
- `debounce*`, `throttle`
|
|
240
337
|
|
|
241
338
|
### Stream (Lazy Iterables)
|
|
242
339
|
- Building: `range`
|
|
243
340
|
- Transforms: `map`, `filter`, `flatMap`, `flatten`
|
|
244
|
-
- Slicing: `take`, `drop`, `
|
|
245
|
-
- Queries
|
|
246
|
-
-
|
|
341
|
+
- Slicing: `take`, `drop`, `chunk`
|
|
342
|
+
- Queries: `find`, `some`, `every`, `reduce`
|
|
343
|
+
- Combining: `zip`, `concat`
|
|
247
344
|
- Utilities: `toArray`, `toAsync`
|
|
248
345
|
|
|
249
|
-
###
|
|
250
|
-
- Math: `add`, `sub`, `mul`, `div`, `
|
|
251
|
-
- String: `split`, `join`, `replace`, `
|
|
346
|
+
### Others
|
|
347
|
+
- Math: `add`, `sub`, `mul`, `div`, `clamp`
|
|
348
|
+
- String: `split`, `join`, `replace`, `trim`
|
|
252
349
|
- Equality: `equals`, `isNil`
|
|
253
350
|
- Debug: `assert`, `invariant`
|
|
254
351
|
|
|
255
352
|
---
|
|
256
353
|
|
|
257
|
-
## Micro-Patterns
|
|
258
|
-
|
|
259
|
-
### Boundary handling (UI/event/service)
|
|
354
|
+
## Micro-Patterns
|
|
260
355
|
|
|
261
|
-
|
|
356
|
+
### Boundary handling
|
|
262
357
|
|
|
263
358
|
```ts
|
|
264
|
-
|
|
359
|
+
const pipeline = pipeSideEffectStrict(validate, process);
|
|
265
360
|
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const result = pipeline(n);
|
|
271
|
-
if (isSideEffect(result)) return runPipeResult(result); // 'NEG'
|
|
272
|
-
return result; // number
|
|
361
|
+
export const handler = (data) => {
|
|
362
|
+
const result = pipeline(data);
|
|
363
|
+
if (isSideEffect(result)) return runPipeResult(result);
|
|
364
|
+
return result;
|
|
273
365
|
};
|
|
274
366
|
```
|
|
275
367
|
|
|
276
|
-
### Data-first
|
|
277
|
-
|
|
278
|
-
Some agents/users prefer “data-first” pipelines. You can keep `pipe` data-last and still write data-first flows by injecting the initial value with `from()`:
|
|
368
|
+
### Data-first with from()
|
|
279
369
|
|
|
280
370
|
```ts
|
|
281
|
-
import { pipe, from, map, filter, take } from 'fp-pack';
|
|
282
|
-
|
|
283
|
-
// data-first feeling: start with data, then compose transforms
|
|
284
371
|
const result = pipe(
|
|
285
372
|
from([1, 2, 3, 4, 5]),
|
|
286
|
-
filter((n: number) => n % 2 === 0),
|
|
287
|
-
map((n) => n * 10),
|
|
288
|
-
take(2)
|
|
289
|
-
)(); // [20, 40]
|
|
290
|
-
|
|
291
|
-
// same idea when you already have an input (normal usage)
|
|
292
|
-
const process = pipe(
|
|
293
373
|
filter((n: number) => n % 2 === 0),
|
|
294
374
|
map((n) => n * 10)
|
|
295
|
-
);
|
|
296
|
-
const result2 = process([1, 2, 3, 4, 5]); // [20, 40]
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
### `ifElse` uses functions (not values)
|
|
300
|
-
|
|
301
|
-
```ts
|
|
302
|
-
import { ifElse, from } from 'fp-pack';
|
|
303
|
-
|
|
304
|
-
const label = ifElse(
|
|
305
|
-
(n: number) => n >= 60,
|
|
306
|
-
from('pass'),
|
|
307
|
-
from('fail')
|
|
308
|
-
);
|
|
375
|
+
)(); // [20, 40]
|
|
309
376
|
```
|
|
310
377
|
|
|
311
|
-
### Stream
|
|
378
|
+
### Stream to array
|
|
312
379
|
|
|
313
380
|
```ts
|
|
314
|
-
import { pipe } from 'fp-pack';
|
|
315
|
-
import { map, filter, toArray } from 'fp-pack/stream';
|
|
316
|
-
|
|
317
381
|
const toIds = pipe(
|
|
318
382
|
filter((u: User) => u.active),
|
|
319
383
|
map((u) => u.id),
|
|
@@ -321,46 +385,9 @@ const toIds = pipe(
|
|
|
321
385
|
);
|
|
322
386
|
```
|
|
323
387
|
|
|
324
|
-
###
|
|
325
|
-
|
|
326
|
-
Use a wrapper or `pipeHint` when a data-last generic won’t infer inside `pipe`:
|
|
327
|
-
|
|
328
|
-
```ts
|
|
329
|
-
import { pipe, pipeHint, zip } from 'fp-pack';
|
|
330
|
-
|
|
331
|
-
// wrapper
|
|
332
|
-
const zipUsers = pipe((xs: User[]) => zip(['a', 'b', 'c'], xs));
|
|
333
|
-
|
|
334
|
-
// pipeHint
|
|
335
|
-
const zipUsers2 = pipe(
|
|
336
|
-
pipeHint<User[], Array<[string, User]>>(zip(['a', 'b', 'c']))
|
|
337
|
-
);
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
### Async pipelines (retry/timeout)
|
|
341
|
-
|
|
342
|
-
Keep async steps inside `pipeAsync` and push configuration (like `timeout`) via currying:
|
|
388
|
+
### Object updates
|
|
343
389
|
|
|
344
390
|
```ts
|
|
345
|
-
import { pipeAsync, retry, timeout } from 'fp-pack';
|
|
346
|
-
|
|
347
|
-
const fetchJson = pipeAsync(
|
|
348
|
-
(url: string) => fetch(url),
|
|
349
|
-
(res) => res.json()
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
const fetchJsonWithGuards = pipeAsync(
|
|
353
|
-
fetchJson,
|
|
354
|
-
timeout(5_000),
|
|
355
|
-
retry(3)
|
|
356
|
-
);
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
### Object updates (avoid mutation)
|
|
360
|
-
|
|
361
|
-
```ts
|
|
362
|
-
import { pipe, assocPath, merge } from 'fp-pack';
|
|
363
|
-
|
|
364
391
|
const updateAccount = pipe(
|
|
365
392
|
assocPath(['profile', 'role'], 'member'),
|
|
366
393
|
merge({ updatedAt: Date.now() })
|
|
@@ -369,71 +396,40 @@ const updateAccount = pipe(
|
|
|
369
396
|
|
|
370
397
|
---
|
|
371
398
|
|
|
372
|
-
## Decision Guide
|
|
399
|
+
## Decision Guide
|
|
373
400
|
|
|
374
401
|
- Is everything sync and pure? → `pipe`
|
|
375
402
|
- Any step async? → `pipeAsync`
|
|
376
403
|
- Need early-exit + typed effect unions? → `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
|
|
377
|
-
- Need early-exit but type precision doesn
|
|
378
|
-
- Handling result at boundary?
|
|
379
|
-
- Need exact branch types? → `isSideEffect` + separate branches
|
|
380
|
-
- Just want to “unwrap” and don’t care about precision? → `runPipeResult` (provide generics if widened)
|
|
404
|
+
- Need early-exit but type precision doesn't matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
|
|
405
|
+
- Handling result at boundary? → `isSideEffect` for branching, `runPipeResult` to unwrap
|
|
381
406
|
- Large/unbounded/iterable data? → `fp-pack/stream`
|
|
382
407
|
|
|
383
408
|
---
|
|
384
409
|
|
|
385
|
-
##
|
|
386
|
-
|
|
387
|
-
### Import Paths
|
|
410
|
+
## Import Paths
|
|
388
411
|
|
|
389
412
|
- Main: `import { pipe, map, filter } from 'fp-pack'`
|
|
390
|
-
- SideEffect
|
|
413
|
+
- SideEffect: `import { pipeSideEffect, SideEffect } from 'fp-pack'`
|
|
391
414
|
- Async: `import { pipeAsync, retry, timeout } from 'fp-pack'`
|
|
392
415
|
- Stream: `import { map, filter, toArray } from 'fp-pack/stream'`
|
|
393
416
|
|
|
394
|
-
### When to Use What
|
|
395
|
-
|
|
396
|
-
- Pure sync transforms: `pipe`
|
|
397
|
-
- Pure async transforms: `pipeAsync`
|
|
398
|
-
- Early-exit pipelines: `pipeSideEffect*` / `pipeAsyncSideEffect*`
|
|
399
|
-
- Strict effect unions: prefer `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
|
|
400
|
-
- Runtime branching: prefer `isSideEffect`
|
|
401
|
-
- Boundary unwrapping: `runPipeResult` (provide generics if inference is widened)
|
|
402
|
-
- Large/unbounded data: `fp-pack/stream`
|
|
403
|
-
|
|
404
|
-
---
|
|
405
|
-
|
|
406
|
-
## Anti-Patterns (Avoid)
|
|
407
|
-
|
|
408
|
-
- Imperative loops when a pipeline is clearer
|
|
409
|
-
- Mutating inputs (prefer immutable transforms)
|
|
410
|
-
- Chaining `Array.prototype.*` in complex transforms (prefer `pipe`)
|
|
411
|
-
- Calling `runPipeResult` inside a SideEffect-aware pipeline
|
|
412
|
-
- Adding “classic monads” to emulate Option/Either instead of using `SideEffect` pipes when necessary
|
|
413
|
-
|
|
414
417
|
---
|
|
415
418
|
|
|
416
419
|
## Quick Signature Lookup (When Unsure)
|
|
417
420
|
|
|
418
|
-
|
|
421
|
+
If TypeScript inference is stuck or you need to verify a function signature:
|
|
422
|
+
|
|
423
|
+
**In fp-pack project:**
|
|
419
424
|
- Main types: `dist/index.d.ts`
|
|
420
425
|
- Stream types: `dist/stream/index.d.ts`
|
|
421
426
|
|
|
422
|
-
|
|
423
|
-
- `node_modules/fp-pack/dist/index.d.ts`
|
|
424
|
-
- `node_modules/fp-pack/dist/stream/index.d.ts`
|
|
425
|
-
|
|
426
|
-
---
|
|
427
|
-
|
|
428
|
-
## Writing New Helpers (If Needed)
|
|
429
|
-
|
|
430
|
-
If you add your own utilities that should compose well:
|
|
431
|
-
- Keep them **unary** for pipelines (or return a unary function via currying).
|
|
432
|
-
- Prefer **data-last** argument order.
|
|
433
|
-
- For generic/overloaded helpers, consider providing an explicit type alias to preserve inference in TypeScript.
|
|
427
|
+
**In consumer project:**
|
|
428
|
+
- Main types: `node_modules/fp-pack/dist/index.d.ts`
|
|
429
|
+
- Stream types: `node_modules/fp-pack/dist/stream/index.d.ts`
|
|
434
430
|
|
|
435
431
|
---
|
|
436
432
|
|
|
437
433
|
## Summary
|
|
438
434
|
|
|
439
|
-
Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary
|
|
435
|
+
Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary.
|
package/dist/skills/fp-pack.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# fp-pack AI Agent Skills
|
|
2
2
|
|
|
3
|
-
Document Version: 0.9.
|
|
3
|
+
Document Version: 0.9.3
|
|
4
4
|
|
|
5
5
|
## ⚠️ Activation Condition (Read First)
|
|
6
6
|
|
|
@@ -11,7 +11,7 @@ Before following this document:
|
|
|
11
11
|
- Check `node_modules/fp-pack` exists
|
|
12
12
|
- Check existing code imports from `fp-pack` or `fp-pack/stream`
|
|
13
13
|
|
|
14
|
-
If `fp-pack` is **not** installed, use the project
|
|
14
|
+
If `fp-pack` is **not** installed, use the project's existing conventions. Do **not** suggest adding fp-pack unless the user asks.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
@@ -23,16 +23,159 @@ When writing or editing code in an fp-pack project:
|
|
|
23
23
|
- Use `stream/*` for lazy/large/iterable processing; use array/object utils for small/eager data.
|
|
24
24
|
- Use SideEffect-aware pipes only when you need **early exit**: `pipeSideEffect*` / `pipeAsyncSideEffect*`.
|
|
25
25
|
- **Never call `runPipeResult` inside a pipeline**; call it at the boundary (event handler, service wrapper, etc.).
|
|
26
|
-
- If TypeScript inference gets stuck with a data-last generic, use a small wrapper
|
|
26
|
+
- If TypeScript inference gets stuck with a data-last generic, use a small wrapper or `pipeHint`.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ⛔ CRITICAL MISTAKES TO AVOID (Read This First!)
|
|
31
|
+
|
|
32
|
+
Before you write any code, know these anti-patterns. These are the most common mistakes that break fp-pack pipelines:
|
|
33
|
+
|
|
34
|
+
### ❌ WRONG: Using `() => value` for constants in pipelines
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// ❌ BAD - Type inference fails
|
|
38
|
+
const broken = pipe(
|
|
39
|
+
() => [1, 2, 3, 4, 5],
|
|
40
|
+
filter((n: number) => n % 2 === 0) // Error!
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// ✅ CORRECT - Use from() for constant values
|
|
44
|
+
const works = pipe(
|
|
45
|
+
from([1, 2, 3, 4, 5]),
|
|
46
|
+
filter((n: number) => n % 2 === 0)
|
|
47
|
+
);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### ❌ WRONG: Using raw values with ifElse/cond
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// ❌ BAD
|
|
54
|
+
const broken = ifElse((score: number) => score >= 60, 'pass', 'fail');
|
|
55
|
+
|
|
56
|
+
// ✅ CORRECT - Use from() to wrap constant values
|
|
57
|
+
const works = ifElse((score: number) => score >= 60, from('pass'), from('fail'));
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### ❌ WRONG: Calling runPipeResult inside a pipeline
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// ❌ BAD
|
|
64
|
+
const broken = pipe(validateUser, runPipeResult, processUser);
|
|
65
|
+
|
|
66
|
+
// ✅ CORRECT - Call runPipeResult OUTSIDE the pipeline
|
|
67
|
+
const pipeline = pipeSideEffect(validateUser, processUser);
|
|
68
|
+
const result = runPipeResult(pipeline(userData));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### ❌ WRONG: Using pipe with SideEffect-returning functions
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// ❌ BAD
|
|
75
|
+
const broken = pipe(
|
|
76
|
+
findUser, // Returns User | SideEffect
|
|
77
|
+
(user) => user.email // Error! SideEffect has no 'email'
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// ✅ CORRECT - Use pipeSideEffect for SideEffect handling
|
|
81
|
+
const works = pipeSideEffect(
|
|
82
|
+
findUser,
|
|
83
|
+
(user) => user.email // Automatically skipped if SideEffect
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### ❌ WRONG: Mutating arrays/objects
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
// ❌ BAD
|
|
91
|
+
users.push(newUser);
|
|
92
|
+
user.name = 'Updated';
|
|
93
|
+
|
|
94
|
+
// ✅ CORRECT - Use immutable operations
|
|
95
|
+
const updatedUsers = append(newUser, users);
|
|
96
|
+
const updatedUser = assoc('name', 'Updated', user);
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### ❌ WRONG: Imperative loops instead of declarative transforms
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// ❌ BAD
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const user of users) {
|
|
105
|
+
if (user.active) results.push(user.name.toUpperCase());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ✅ CORRECT
|
|
109
|
+
const results = pipe(
|
|
110
|
+
filter((user: User) => user.active),
|
|
111
|
+
map(user => user.name.toUpperCase())
|
|
112
|
+
)(users);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Remember:** If you encounter any of these patterns, STOP and use the correct fp-pack approach instead!
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 💡 Real-World Examples (Learn by Doing)
|
|
120
|
+
|
|
121
|
+
Study these common patterns first:
|
|
122
|
+
|
|
123
|
+
### Example 1: User Data Processing Pipeline
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { pipe, filter, map, take, sortBy } from 'fp-pack';
|
|
127
|
+
|
|
128
|
+
const getTopActiveUsers = pipe(
|
|
129
|
+
filter((user: User) => user.active && user.lastLogin > cutoffDate),
|
|
130
|
+
sortBy((user) => -user.activityScore),
|
|
131
|
+
map(user => ({ id: user.id, name: user.name, score: user.activityScore })),
|
|
132
|
+
take(10)
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const topUsers = getTopActiveUsers(allUsers);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Example 2: API Request with Error Handling
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { pipeAsyncSideEffect, SideEffect, runPipeResult } from 'fp-pack';
|
|
142
|
+
|
|
143
|
+
const fetchUserData = pipeAsyncSideEffect(
|
|
144
|
+
async (userId: string) => {
|
|
145
|
+
const res = await fetch(`/api/users/${userId}`);
|
|
146
|
+
return res.ok ? res : SideEffect.of(() => `HTTP ${res.status}`);
|
|
147
|
+
},
|
|
148
|
+
async (res) => res.json(),
|
|
149
|
+
(data) => data.verified ? data : SideEffect.of(() => 'User not verified')
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Call at boundary
|
|
153
|
+
const result = runPipeResult(await fetchUserData('user-123'));
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Example 3: Data Transformation with from()
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { pipe, from, ifElse, filter, map } from 'fp-pack';
|
|
160
|
+
|
|
161
|
+
// Constant value injection
|
|
162
|
+
const getStatusMessage = ifElse(
|
|
163
|
+
(count: number) => count > 0,
|
|
164
|
+
from('Items available'),
|
|
165
|
+
from('No items found')
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Data-first pattern
|
|
169
|
+
const processWithData = pipe(
|
|
170
|
+
from([1, 2, 3, 4, 5]),
|
|
171
|
+
filter((n: number) => n % 2 === 0),
|
|
172
|
+
map(n => n * 2)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const result = processWithData(); // [4, 8]
|
|
176
|
+
```
|
|
29
177
|
|
|
30
|
-
|
|
31
|
-
1) Function composition via `pipe` / `pipeAsync`
|
|
32
|
-
2) Declarative, immutable transforms (avoid loops and mutations)
|
|
33
|
-
3) Practical TypeScript inference (data-last + currying for most helpers)
|
|
34
|
-
4) Optional early-exit handling via `SideEffect` (only when needed)
|
|
35
|
-
5) Lazy iterable processing via `fp-pack/stream` for performance-sensitive flows
|
|
178
|
+
**These patterns cover 90% of real-world use cases!**
|
|
36
179
|
|
|
37
180
|
---
|
|
38
181
|
|
|
@@ -40,8 +183,6 @@ fp-pack is a TypeScript FP utility library focused on:
|
|
|
40
183
|
|
|
41
184
|
### `pipe` (sync)
|
|
42
185
|
|
|
43
|
-
Use `pipe` to build a unary function:
|
|
44
|
-
|
|
45
186
|
```ts
|
|
46
187
|
import { pipe, filter, map, take } from 'fp-pack';
|
|
47
188
|
|
|
@@ -54,8 +195,6 @@ const processUsers = pipe(
|
|
|
54
195
|
|
|
55
196
|
### `pipeAsync` (async)
|
|
56
197
|
|
|
57
|
-
Use `pipeAsync` when any step is async:
|
|
58
|
-
|
|
59
198
|
```ts
|
|
60
199
|
import { pipeAsync } from 'fp-pack';
|
|
61
200
|
|
|
@@ -68,120 +207,82 @@ const fetchUser = pipeAsync(
|
|
|
68
207
|
|
|
69
208
|
---
|
|
70
209
|
|
|
71
|
-
## Currying & Data-Last
|
|
72
|
-
|
|
73
|
-
Most multi-arg helpers are **data-last** and **curried**, so they work naturally in `pipe`:
|
|
210
|
+
## Currying & Data-Last
|
|
74
211
|
|
|
212
|
+
Most multi-arg helpers are **data-last** and **curried**:
|
|
75
213
|
- Good: `map(fn)`, `filter(pred)`, `replace(from, to)`, `assoc('k', v)`, `path(['a','b'])`
|
|
76
|
-
-
|
|
77
|
-
|
|
78
|
-
Single-arg helpers are already unary; “currying them” adds no value—just use them directly.
|
|
214
|
+
- Single-arg helpers are already unary—just use them directly
|
|
79
215
|
|
|
80
216
|
---
|
|
81
217
|
|
|
82
|
-
## TypeScript: Data-last Generic Inference
|
|
218
|
+
## TypeScript: Data-last Generic Inference
|
|
83
219
|
|
|
84
|
-
Some data-last helpers return a **generic function** whose type is only determined by the final data argument.
|
|
85
|
-
Inside `pipe`/`pipeAsync`, TypeScript sometimes can’t infer that type, so you may need a tiny hint.
|
|
220
|
+
Some data-last helpers return a **generic function** whose type is only determined by the final data argument. Inside `pipe`, TypeScript sometimes can't infer that type.
|
|
86
221
|
|
|
87
|
-
###
|
|
222
|
+
### Quick fixes (choose 1)
|
|
88
223
|
|
|
89
224
|
```ts
|
|
90
225
|
import { pipe, pipeHint, zip, some } from 'fp-pack';
|
|
91
226
|
|
|
92
|
-
// 1) data-first wrapper
|
|
227
|
+
// 1) data-first wrapper
|
|
93
228
|
const withWrapper = pipe(
|
|
94
229
|
(values: number[]) => zip([1, 2, 3], values),
|
|
95
230
|
some(([a, b]) => a > b)
|
|
96
231
|
);
|
|
97
232
|
|
|
98
|
-
// 2)
|
|
99
|
-
const withHint = pipe(
|
|
100
|
-
zip([1, 2, 3]) as (values: number[]) => Array<[number, number]>,
|
|
101
|
-
some(([a, b]) => a > b)
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// 3) pipeHint helper (keeps pipeline style)
|
|
233
|
+
// 2) pipeHint helper
|
|
105
234
|
const withPipeHint = pipe(
|
|
106
235
|
pipeHint<number[], Array<[number, number]>>(zip([1, 2, 3])),
|
|
107
236
|
some(([a, b]) => a > b)
|
|
108
237
|
);
|
|
109
238
|
```
|
|
110
239
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
- Array: `chunk`, `drop`, `take`, `zip`
|
|
114
|
-
- Object: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
|
|
115
|
-
- Async: `timeout`
|
|
116
|
-
- Stream: `chunk`, `drop`, `take`, `zip`
|
|
117
|
-
|
|
118
|
-
When in doubt: check the `.d.ts` signature (see “Quick Signature Lookup”).
|
|
240
|
+
**Utilities that may need a hint:** `chunk`, `drop`, `take`, `zip`, `assoc`, `path`, `prop`, `timeout`
|
|
119
241
|
|
|
120
242
|
---
|
|
121
243
|
|
|
122
244
|
## SideEffect Pattern (Use Only When Needed)
|
|
123
245
|
|
|
124
|
-
Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you
|
|
246
|
+
Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you need **early termination**:
|
|
125
247
|
- validation pipelines that should stop early
|
|
126
248
|
- recoverable errors you want to model as data
|
|
127
249
|
- branching flows where you want to short-circuit
|
|
128
250
|
|
|
129
251
|
### SideEffect-aware pipes
|
|
130
252
|
- `pipeSideEffect` / `pipeAsyncSideEffect`: convenient, but may widen effects to `any`
|
|
131
|
-
- `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended
|
|
132
|
-
|
|
133
|
-
### Rules
|
|
134
|
-
- Do **not** call `runPipeResult` or `matchSideEffect` **inside** the pipeline.
|
|
135
|
-
- Prefer `isSideEffect` for precise runtime narrowing in both branches.
|
|
136
|
-
- Use `runPipeResult` only at the boundary, and provide generics if inference is widened.
|
|
253
|
+
- `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended)
|
|
137
254
|
|
|
138
255
|
### Key functions
|
|
139
256
|
- `SideEffect.of(effectFn, label?)`
|
|
140
257
|
- `isSideEffect(value)` (type guard)
|
|
141
258
|
- `runPipeResult(result)` (execute effect or return value; **outside** pipelines)
|
|
142
|
-
- `matchSideEffect(result, { value, effect })`
|
|
143
|
-
|
|
144
|
-
### `runPipeResult` type behavior (important)
|
|
145
259
|
|
|
146
|
-
|
|
147
|
-
- If the input is **widened** to `SideEffect<any>` or `any` (common with non-strict pipelines), `runPipeResult(x)` becomes `any` unless you provide generics.
|
|
260
|
+
### Example
|
|
148
261
|
|
|
149
262
|
```ts
|
|
150
|
-
import {
|
|
151
|
-
pipeSideEffect,
|
|
152
|
-
pipeSideEffectStrict,
|
|
153
|
-
SideEffect,
|
|
154
|
-
isSideEffect,
|
|
155
|
-
runPipeResult,
|
|
156
|
-
} from 'fp-pack';
|
|
157
|
-
|
|
158
|
-
const nonStrict = pipeSideEffect(
|
|
159
|
-
(n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
|
|
160
|
-
);
|
|
161
|
-
const widened: number | SideEffect<any> = nonStrict(-1);
|
|
162
|
-
const unsafe = runPipeResult(widened); // any
|
|
263
|
+
import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
|
|
163
264
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
265
|
+
const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
|
|
266
|
+
const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
|
|
267
|
+
|
|
268
|
+
const result = pipeline(-1); // number | SideEffect<'NEG'>
|
|
168
269
|
|
|
169
|
-
if (isSideEffect(
|
|
170
|
-
const err = runPipeResult(
|
|
270
|
+
if (isSideEffect(result)) {
|
|
271
|
+
const err = runPipeResult(result); // 'NEG'
|
|
272
|
+
} else {
|
|
273
|
+
// result is number
|
|
171
274
|
}
|
|
172
275
|
```
|
|
173
276
|
|
|
174
277
|
---
|
|
175
278
|
|
|
176
|
-
## Stream Functions (`fp-pack/stream`)
|
|
279
|
+
## Stream Functions (`fp-pack/stream`)
|
|
177
280
|
|
|
178
281
|
Use stream utilities when:
|
|
179
|
-
- data is large or unbounded
|
|
180
|
-
- you want lazy evaluation
|
|
282
|
+
- data is large or unbounded
|
|
283
|
+
- you want lazy evaluation
|
|
181
284
|
- you want to support `Iterable` and `AsyncIterable`
|
|
182
285
|
|
|
183
|
-
Stream utilities are in `fp-pack/stream` and are designed to be pipe-friendly.
|
|
184
|
-
|
|
185
286
|
```ts
|
|
186
287
|
import { pipe } from 'fp-pack';
|
|
187
288
|
import { range, filter, map, take, toArray } from 'fp-pack/stream';
|
|
@@ -198,115 +299,78 @@ const result = first100SquaresOfEvens(range(Infinity));
|
|
|
198
299
|
|
|
199
300
|
---
|
|
200
301
|
|
|
201
|
-
## Available Functions (
|
|
202
|
-
|
|
203
|
-
This is not a complete API reference; it’s the “what to reach for” index when composing pipelines.
|
|
302
|
+
## Available Functions (Quick Index)
|
|
204
303
|
|
|
205
304
|
### Composition
|
|
206
305
|
- `pipe`, `pipeAsync`
|
|
207
306
|
- SideEffect-aware: `pipeSideEffect`, `pipeSideEffectStrict`, `pipeAsyncSideEffect`, `pipeAsyncSideEffectStrict`
|
|
208
|
-
- Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `
|
|
307
|
+
- Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `curry`, `compose`
|
|
209
308
|
- SideEffect helpers: `SideEffect`, `isSideEffect`, `matchSideEffect`, `runPipeResult`
|
|
210
309
|
|
|
211
310
|
### Array
|
|
212
|
-
-
|
|
311
|
+
- Transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
|
|
213
312
|
- Queries: `find`, `some`, `every`
|
|
214
|
-
- Slicing: `take`, `drop`, `
|
|
215
|
-
- Ordering
|
|
216
|
-
-
|
|
217
|
-
- Combining: `concat`, `append`, `prepend`, `flatten`
|
|
313
|
+
- Slicing: `take`, `drop`, `chunk`
|
|
314
|
+
- Ordering: `sort`, `sortBy`, `groupBy`, `uniqBy`
|
|
315
|
+
- Combining: `zip`, `concat`, `append`, `flatten`
|
|
218
316
|
|
|
219
317
|
### Object
|
|
220
|
-
- Access: `prop`, `
|
|
318
|
+
- Access: `prop`, `path`, `propOr`, `pathOr`
|
|
221
319
|
- Pick/drop: `pick`, `omit`
|
|
222
320
|
- Updates: `assoc`, `assocPath`, `dissocPath`
|
|
223
321
|
- Merge: `merge`, `mergeDeep`
|
|
224
322
|
- Transforms: `mapValues`, `evolve`
|
|
225
|
-
- Predicates: `has`
|
|
226
323
|
|
|
227
324
|
### Control Flow
|
|
228
325
|
- `ifElse`, `when`, `unless`, `cond`, `guard`, `tryCatch`
|
|
229
326
|
|
|
230
327
|
### Async
|
|
231
328
|
- `retry`, `timeout`, `delay`
|
|
232
|
-
-
|
|
329
|
+
- `debounce*`, `throttle`
|
|
233
330
|
|
|
234
331
|
### Stream (Lazy Iterables)
|
|
235
332
|
- Building: `range`
|
|
236
333
|
- Transforms: `map`, `filter`, `flatMap`, `flatten`
|
|
237
|
-
- Slicing: `take`, `drop`, `
|
|
238
|
-
- Queries
|
|
239
|
-
-
|
|
334
|
+
- Slicing: `take`, `drop`, `chunk`
|
|
335
|
+
- Queries: `find`, `some`, `every`, `reduce`
|
|
336
|
+
- Combining: `zip`, `concat`
|
|
240
337
|
- Utilities: `toArray`, `toAsync`
|
|
241
338
|
|
|
242
|
-
###
|
|
243
|
-
- Math: `add`, `sub`, `mul`, `div`, `
|
|
244
|
-
- String: `split`, `join`, `replace`, `
|
|
339
|
+
### Others
|
|
340
|
+
- Math: `add`, `sub`, `mul`, `div`, `clamp`
|
|
341
|
+
- String: `split`, `join`, `replace`, `trim`
|
|
245
342
|
- Equality: `equals`, `isNil`
|
|
246
343
|
- Debug: `assert`, `invariant`
|
|
247
344
|
|
|
248
345
|
---
|
|
249
346
|
|
|
250
|
-
## Micro-Patterns
|
|
251
|
-
|
|
252
|
-
### Boundary handling (UI/event/service)
|
|
347
|
+
## Micro-Patterns
|
|
253
348
|
|
|
254
|
-
|
|
349
|
+
### Boundary handling
|
|
255
350
|
|
|
256
351
|
```ts
|
|
257
|
-
|
|
352
|
+
const pipeline = pipeSideEffectStrict(validate, process);
|
|
258
353
|
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const result = pipeline(n);
|
|
264
|
-
if (isSideEffect(result)) return runPipeResult(result); // 'NEG'
|
|
265
|
-
return result; // number
|
|
354
|
+
export const handler = (data) => {
|
|
355
|
+
const result = pipeline(data);
|
|
356
|
+
if (isSideEffect(result)) return runPipeResult(result);
|
|
357
|
+
return result;
|
|
266
358
|
};
|
|
267
359
|
```
|
|
268
360
|
|
|
269
|
-
### Data-first
|
|
270
|
-
|
|
271
|
-
Some agents/users prefer “data-first” pipelines. You can keep `pipe` data-last and still write data-first flows by injecting the initial value with `from()`:
|
|
361
|
+
### Data-first with from()
|
|
272
362
|
|
|
273
363
|
```ts
|
|
274
|
-
import { pipe, from, map, filter, take } from 'fp-pack';
|
|
275
|
-
|
|
276
|
-
// data-first feeling: start with data, then compose transforms
|
|
277
364
|
const result = pipe(
|
|
278
365
|
from([1, 2, 3, 4, 5]),
|
|
279
|
-
filter((n: number) => n % 2 === 0),
|
|
280
|
-
map((n) => n * 10),
|
|
281
|
-
take(2)
|
|
282
|
-
)(); // [20, 40]
|
|
283
|
-
|
|
284
|
-
// same idea when you already have an input (normal usage)
|
|
285
|
-
const process = pipe(
|
|
286
366
|
filter((n: number) => n % 2 === 0),
|
|
287
367
|
map((n) => n * 10)
|
|
288
|
-
);
|
|
289
|
-
const result2 = process([1, 2, 3, 4, 5]); // [20, 40]
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### `ifElse` uses functions (not values)
|
|
293
|
-
|
|
294
|
-
```ts
|
|
295
|
-
import { ifElse, from } from 'fp-pack';
|
|
296
|
-
|
|
297
|
-
const label = ifElse(
|
|
298
|
-
(n: number) => n >= 60,
|
|
299
|
-
from('pass'),
|
|
300
|
-
from('fail')
|
|
301
|
-
);
|
|
368
|
+
)(); // [20, 40]
|
|
302
369
|
```
|
|
303
370
|
|
|
304
|
-
### Stream
|
|
371
|
+
### Stream to array
|
|
305
372
|
|
|
306
373
|
```ts
|
|
307
|
-
import { pipe } from 'fp-pack';
|
|
308
|
-
import { map, filter, toArray } from 'fp-pack/stream';
|
|
309
|
-
|
|
310
374
|
const toIds = pipe(
|
|
311
375
|
filter((u: User) => u.active),
|
|
312
376
|
map((u) => u.id),
|
|
@@ -314,46 +378,9 @@ const toIds = pipe(
|
|
|
314
378
|
);
|
|
315
379
|
```
|
|
316
380
|
|
|
317
|
-
###
|
|
318
|
-
|
|
319
|
-
Use a wrapper or `pipeHint` when a data-last generic won’t infer inside `pipe`:
|
|
320
|
-
|
|
321
|
-
```ts
|
|
322
|
-
import { pipe, pipeHint, zip } from 'fp-pack';
|
|
323
|
-
|
|
324
|
-
// wrapper
|
|
325
|
-
const zipUsers = pipe((xs: User[]) => zip(['a', 'b', 'c'], xs));
|
|
326
|
-
|
|
327
|
-
// pipeHint
|
|
328
|
-
const zipUsers2 = pipe(
|
|
329
|
-
pipeHint<User[], Array<[string, User]>>(zip(['a', 'b', 'c']))
|
|
330
|
-
);
|
|
331
|
-
```
|
|
332
|
-
|
|
333
|
-
### Async pipelines (retry/timeout)
|
|
334
|
-
|
|
335
|
-
Keep async steps inside `pipeAsync` and push configuration (like `timeout`) via currying:
|
|
381
|
+
### Object updates
|
|
336
382
|
|
|
337
383
|
```ts
|
|
338
|
-
import { pipeAsync, retry, timeout } from 'fp-pack';
|
|
339
|
-
|
|
340
|
-
const fetchJson = pipeAsync(
|
|
341
|
-
(url: string) => fetch(url),
|
|
342
|
-
(res) => res.json()
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
const fetchJsonWithGuards = pipeAsync(
|
|
346
|
-
fetchJson,
|
|
347
|
-
timeout(5_000),
|
|
348
|
-
retry(3)
|
|
349
|
-
);
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Object updates (avoid mutation)
|
|
353
|
-
|
|
354
|
-
```ts
|
|
355
|
-
import { pipe, assocPath, merge } from 'fp-pack';
|
|
356
|
-
|
|
357
384
|
const updateAccount = pipe(
|
|
358
385
|
assocPath(['profile', 'role'], 'member'),
|
|
359
386
|
merge({ updatedAt: Date.now() })
|
|
@@ -362,71 +389,40 @@ const updateAccount = pipe(
|
|
|
362
389
|
|
|
363
390
|
---
|
|
364
391
|
|
|
365
|
-
## Decision Guide
|
|
392
|
+
## Decision Guide
|
|
366
393
|
|
|
367
394
|
- Is everything sync and pure? → `pipe`
|
|
368
395
|
- Any step async? → `pipeAsync`
|
|
369
396
|
- Need early-exit + typed effect unions? → `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
|
|
370
|
-
- Need early-exit but type precision doesn
|
|
371
|
-
- Handling result at boundary?
|
|
372
|
-
- Need exact branch types? → `isSideEffect` + separate branches
|
|
373
|
-
- Just want to “unwrap” and don’t care about precision? → `runPipeResult` (provide generics if widened)
|
|
397
|
+
- Need early-exit but type precision doesn't matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
|
|
398
|
+
- Handling result at boundary? → `isSideEffect` for branching, `runPipeResult` to unwrap
|
|
374
399
|
- Large/unbounded/iterable data? → `fp-pack/stream`
|
|
375
400
|
|
|
376
401
|
---
|
|
377
402
|
|
|
378
|
-
##
|
|
379
|
-
|
|
380
|
-
### Import Paths
|
|
403
|
+
## Import Paths
|
|
381
404
|
|
|
382
405
|
- Main: `import { pipe, map, filter } from 'fp-pack'`
|
|
383
|
-
- SideEffect
|
|
406
|
+
- SideEffect: `import { pipeSideEffect, SideEffect } from 'fp-pack'`
|
|
384
407
|
- Async: `import { pipeAsync, retry, timeout } from 'fp-pack'`
|
|
385
408
|
- Stream: `import { map, filter, toArray } from 'fp-pack/stream'`
|
|
386
409
|
|
|
387
|
-
### When to Use What
|
|
388
|
-
|
|
389
|
-
- Pure sync transforms: `pipe`
|
|
390
|
-
- Pure async transforms: `pipeAsync`
|
|
391
|
-
- Early-exit pipelines: `pipeSideEffect*` / `pipeAsyncSideEffect*`
|
|
392
|
-
- Strict effect unions: prefer `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
|
|
393
|
-
- Runtime branching: prefer `isSideEffect`
|
|
394
|
-
- Boundary unwrapping: `runPipeResult` (provide generics if inference is widened)
|
|
395
|
-
- Large/unbounded data: `fp-pack/stream`
|
|
396
|
-
|
|
397
|
-
---
|
|
398
|
-
|
|
399
|
-
## Anti-Patterns (Avoid)
|
|
400
|
-
|
|
401
|
-
- Imperative loops when a pipeline is clearer
|
|
402
|
-
- Mutating inputs (prefer immutable transforms)
|
|
403
|
-
- Chaining `Array.prototype.*` in complex transforms (prefer `pipe`)
|
|
404
|
-
- Calling `runPipeResult` inside a SideEffect-aware pipeline
|
|
405
|
-
- Adding “classic monads” to emulate Option/Either instead of using `SideEffect` pipes when necessary
|
|
406
|
-
|
|
407
410
|
---
|
|
408
411
|
|
|
409
412
|
## Quick Signature Lookup (When Unsure)
|
|
410
413
|
|
|
411
|
-
|
|
414
|
+
If TypeScript inference is stuck or you need to verify a function signature:
|
|
415
|
+
|
|
416
|
+
**In fp-pack project:**
|
|
412
417
|
- Main types: `dist/index.d.ts`
|
|
413
418
|
- Stream types: `dist/stream/index.d.ts`
|
|
414
419
|
|
|
415
|
-
|
|
416
|
-
- `node_modules/fp-pack/dist/index.d.ts`
|
|
417
|
-
- `node_modules/fp-pack/dist/stream/index.d.ts`
|
|
418
|
-
|
|
419
|
-
---
|
|
420
|
-
|
|
421
|
-
## Writing New Helpers (If Needed)
|
|
422
|
-
|
|
423
|
-
If you add your own utilities that should compose well:
|
|
424
|
-
- Keep them **unary** for pipelines (or return a unary function via currying).
|
|
425
|
-
- Prefer **data-last** argument order.
|
|
426
|
-
- For generic/overloaded helpers, consider providing an explicit type alias to preserve inference in TypeScript.
|
|
420
|
+
**In consumer project:**
|
|
421
|
+
- Main types: `node_modules/fp-pack/dist/index.d.ts`
|
|
422
|
+
- Stream types: `node_modules/fp-pack/dist/stream/index.d.ts`
|
|
427
423
|
|
|
428
424
|
---
|
|
429
425
|
|
|
430
426
|
## Summary
|
|
431
427
|
|
|
432
|
-
Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary
|
|
428
|
+
Default to `pipe` / `pipeAsync`, keep helpers data-last and unary, switch to `stream/*` when laziness matters, and reserve SideEffect-aware pipelines for true early-exit flows. Use `isSideEffect` for precise narrowing and call `runPipeResult` only at the boundary.
|