fp-pack 0.9.0 → 0.9.2
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 +10 -0
- package/dist/ai-addons/fp-pack-agent-addon.md +292 -0
- package/dist/skills/fp-pack/SKILL.md +272 -1579
- package/dist/skills/fp-pack.md +271 -1578
- package/package.json +4 -2
|
@@ -7,1740 +7,433 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# fp-pack AI Agent Skills
|
|
9
9
|
|
|
10
|
-
Document Version: 0.9.
|
|
10
|
+
Document Version: 0.9.2
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## ⚠️ Activation Condition (Read First)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
These guidelines apply **only when `fp-pack` is installed** in the current project.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Before following this document:
|
|
17
|
+
- Check `package.json` for `fp-pack` in dependencies/devDependencies
|
|
18
|
+
- Check `node_modules/fp-pack` exists
|
|
19
|
+
- Check existing code imports from `fp-pack` or `fp-pack/stream`
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
2. **Declarative Code**: Prefer function composition over imperative loops and mutations
|
|
20
|
-
3. **No Monad Pattern**: Traditional FP monads (Option, Either, etc.) are NOT used - they don't compose well with `pipe`
|
|
21
|
-
4. **SideEffect Pattern**: Handle errors and side effects using `SideEffect` with `pipeSideEffect` / `pipeAsyncSideEffect` pipelines (use `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict` for strict unions)
|
|
22
|
-
5. **Lazy Evaluation**: Use `stream/*` functions for efficient iterable processing
|
|
21
|
+
If `fp-pack` is **not** installed, use the project’s existing conventions. Do **not** suggest adding fp-pack unless the user asks.
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Agent Checklist (TL;DR)
|
|
26
|
+
|
|
27
|
+
When writing or editing code in an fp-pack project:
|
|
28
|
+
- Prefer `pipe` / `pipeAsync` for composition; keep helpers **unary** in the pipeline.
|
|
29
|
+
- Prefer **data-last** utilities (curried) so they compose: `map(fn)`, `prop('k')`, `assoc('k', v)`, etc.
|
|
30
|
+
- Use `stream/*` for lazy/large/iterable processing; use array/object utils for small/eager data.
|
|
31
|
+
- Use SideEffect-aware pipes only when you need **early exit**: `pipeSideEffect*` / `pipeAsyncSideEffect*`.
|
|
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, an explicit type hint, or `pipeHint`.
|
|
34
|
+
|
|
35
|
+
## Philosophy (What fp-pack Optimizes For)
|
|
36
|
+
|
|
37
|
+
fp-pack is a TypeScript FP utility library focused on:
|
|
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
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Core Composition
|
|
25
47
|
|
|
26
|
-
### pipe
|
|
48
|
+
### `pipe` (sync)
|
|
27
49
|
|
|
28
|
-
|
|
50
|
+
Use `pipe` to build a unary function:
|
|
29
51
|
|
|
30
|
-
```
|
|
31
|
-
import { pipe,
|
|
52
|
+
```ts
|
|
53
|
+
import { pipe, filter, map, take } from 'fp-pack';
|
|
32
54
|
|
|
33
|
-
// GOOD: Declarative pipe composition
|
|
34
55
|
const processUsers = pipe(
|
|
35
|
-
filter((
|
|
36
|
-
map(
|
|
56
|
+
filter((u: User) => u.active),
|
|
57
|
+
map((u) => u.name),
|
|
37
58
|
take(10)
|
|
38
59
|
);
|
|
39
|
-
|
|
40
|
-
// BAD: Imperative approach
|
|
41
|
-
const processUsers = (users: User[]) => {
|
|
42
|
-
const result = [];
|
|
43
|
-
for (const user of users) {
|
|
44
|
-
if (user.age >= 18) {
|
|
45
|
-
result.push(user.name.toUpperCase());
|
|
46
|
-
if (result.length >= 10) break;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return result;
|
|
50
|
-
};
|
|
51
60
|
```
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
### `pipeAsync` (async)
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
Use `pipeAsync` when any step is async:
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
66
|
+
```ts
|
|
60
67
|
import { pipeAsync } from 'fp-pack';
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
async (
|
|
65
|
-
async (response) => response.json(),
|
|
69
|
+
const fetchUser = pipeAsync(
|
|
70
|
+
async (id: string) => fetch(`/api/users/${id}`),
|
|
71
|
+
async (res) => res.json(),
|
|
66
72
|
(data) => data.user
|
|
67
73
|
);
|
|
68
|
-
|
|
69
|
-
// BAD: Manual async handling
|
|
70
|
-
const fetchUserData = async (userId: string) => {
|
|
71
|
-
const response = await fetch(`/api/users/${userId}`);
|
|
72
|
-
const data = await response.json();
|
|
73
|
-
return data.user;
|
|
74
|
-
};
|
|
75
74
|
```
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Currying & Data-Last Conventions
|
|
79
|
+
|
|
80
|
+
Most multi-arg helpers are **data-last** and **curried**, so they work naturally in `pipe`:
|
|
81
|
+
|
|
82
|
+
- Good: `map(fn)`, `filter(pred)`, `replace(from, to)`, `assoc('k', v)`, `path(['a','b'])`
|
|
83
|
+
- Avoid in pipelines: helpers that require you to pass the data first, or that return a function whose type depends on the final data arg (see next section)
|
|
84
|
+
|
|
85
|
+
Single-arg helpers are already unary; “currying them” adds no value—just use them directly.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## TypeScript: Data-last Generic Inference Caveats (Important)
|
|
78
90
|
|
|
79
|
-
|
|
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.
|
|
80
93
|
|
|
81
|
-
|
|
82
|
-
`pipe`/`pipeAsync`, TypeScript cannot infer that type, so you may need `pipeHint`, a small type hint, or a data-first wrapper.
|
|
94
|
+
### Recommended fixes (choose 1)
|
|
83
95
|
|
|
84
|
-
```
|
|
96
|
+
```ts
|
|
85
97
|
import { pipe, pipeHint, zip, some } from 'fp-pack';
|
|
86
98
|
|
|
87
|
-
//
|
|
99
|
+
// 1) data-first wrapper (most explicit)
|
|
88
100
|
const withWrapper = pipe(
|
|
89
101
|
(values: number[]) => zip([1, 2, 3], values),
|
|
90
102
|
some(([a, b]) => a > b)
|
|
91
103
|
);
|
|
92
104
|
|
|
93
|
-
//
|
|
105
|
+
// 2) explicit function type hint (short, but uses an assertion)
|
|
94
106
|
const withHint = pipe(
|
|
95
107
|
zip([1, 2, 3]) as (values: number[]) => Array<[number, number]>,
|
|
96
108
|
some(([a, b]) => a > b)
|
|
97
109
|
);
|
|
98
110
|
|
|
99
|
-
//
|
|
111
|
+
// 3) pipeHint helper (keeps pipeline style)
|
|
100
112
|
const withPipeHint = pipe(
|
|
101
113
|
pipeHint<number[], Array<[number, number]>>(zip([1, 2, 3])),
|
|
102
114
|
some(([a, b]) => a > b)
|
|
103
115
|
);
|
|
104
116
|
```
|
|
105
117
|
|
|
106
|
-
|
|
107
|
-
- **Array**: `chunk`, `drop`, `take`, `zip`
|
|
108
|
-
- **Object**: `assoc`, `assocPath`, `dissocPath`, `evolve`, `mapValues`, `merge`, `mergeDeep`, `omit`, `path`, `pathOr`, `pick`, `prop`, `propOr`, `propStrict`
|
|
109
|
-
- **Async**: `timeout`
|
|
110
|
-
- **Stream**: `chunk`, `drop`, `take`, `zip`
|
|
118
|
+
### Utilities that may need a hint in data-last pipelines
|
|
111
119
|
|
|
112
|
-
|
|
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`
|
|
113
124
|
|
|
114
|
-
|
|
125
|
+
When in doubt: check the `.d.ts` signature (see “Quick Signature Lookup”).
|
|
115
126
|
|
|
116
|
-
|
|
117
|
-
- Early termination based on validation
|
|
118
|
-
- Error handling with side effects (logging, toasts, etc.)
|
|
119
|
-
- Optional chaining patterns
|
|
120
|
-
|
|
121
|
-
For regular error handling, standard try-catch or error propagation is perfectly fine.
|
|
122
|
-
If you want precise SideEffect unions across branches, use `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`.
|
|
127
|
+
---
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
// MOST CASES: Just use pipe with regular error handling
|
|
126
|
-
import { pipe, map, filter } from 'fp-pack';
|
|
129
|
+
## SideEffect Pattern (Use Only When Needed)
|
|
127
130
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
131
|
+
Most code should use `pipe` / `pipeAsync`. Use SideEffect-aware pipes only when you truly need **early termination**:
|
|
132
|
+
- validation pipelines that should stop early
|
|
133
|
+
- recoverable errors you want to model as data
|
|
134
|
+
- branching flows where you want to short-circuit
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
console.error('Processing failed:', error);
|
|
138
|
-
}
|
|
136
|
+
### SideEffect-aware pipes
|
|
137
|
+
- `pipeSideEffect` / `pipeAsyncSideEffect`: convenient, but may widen effects to `any`
|
|
138
|
+
- `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`: preserves strict union effects (recommended when you care about types)
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
validateInput,
|
|
145
|
-
(data) => {
|
|
146
|
-
if (!data.isValid) {
|
|
147
|
-
return SideEffect.of(() => {
|
|
148
|
-
showToast('Invalid data'); // Side effect
|
|
149
|
-
logError('validation_failed'); // Side effect
|
|
150
|
-
return null;
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
return data;
|
|
154
|
-
},
|
|
155
|
-
transformData
|
|
156
|
-
);
|
|
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.
|
|
157
144
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
145
|
+
### Key functions
|
|
146
|
+
- `SideEffect.of(effectFn, label?)`
|
|
147
|
+
- `isSideEffect(value)` (type guard)
|
|
148
|
+
- `runPipeResult(result)` (execute effect or return value; **outside** pipelines)
|
|
149
|
+
- `matchSideEffect(result, { value, effect })`
|
|
161
150
|
|
|
162
|
-
|
|
163
|
-
- `SideEffect.of(fn, label?)` - Create a side effect container
|
|
164
|
-
- `isSideEffect(value)` - Type guard for **runtime checking** whether a value is a SideEffect
|
|
165
|
-
- `runPipeResult<T, R>(result)` - Execute SideEffect or return value (call **OUTSIDE** pipelines). If the input is narrowed to `SideEffect<R>` (e.g. after `isSideEffect`), it returns `R`. If the input is widened to `SideEffect<any>` or `any`, the result becomes `any` unless you provide generics.
|
|
166
|
-
- `matchSideEffect(result, { value, effect })` - Pattern match on result
|
|
151
|
+
### `runPipeResult` type behavior (important)
|
|
167
152
|
|
|
168
|
-
|
|
153
|
+
- If the input is **narrowed** to `SideEffect<R>` (e.g. inside `if (isSideEffect(x))`), `runPipeResult(x)` returns `R`.
|
|
154
|
+
- If the input is **widened** to `SideEffect<any>` or `any` (common with non-strict pipelines), `runPipeResult(x)` becomes `any` unless you provide generics.
|
|
169
155
|
|
|
170
|
-
```
|
|
171
|
-
import {
|
|
156
|
+
```ts
|
|
157
|
+
import {
|
|
158
|
+
pipeSideEffect,
|
|
159
|
+
pipeSideEffectStrict,
|
|
160
|
+
SideEffect,
|
|
161
|
+
isSideEffect,
|
|
162
|
+
runPipeResult,
|
|
163
|
+
} from 'fp-pack';
|
|
172
164
|
|
|
173
|
-
const
|
|
174
|
-
(
|
|
175
|
-
(odds) => odds.length > 0
|
|
176
|
-
? odds
|
|
177
|
-
: SideEffect.of(() => 'No odd numbers'),
|
|
178
|
-
(odds) => odds.map(n => n * 2)
|
|
165
|
+
const nonStrict = pipeSideEffect(
|
|
166
|
+
(n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
|
|
179
167
|
);
|
|
168
|
+
const widened: number | SideEffect<any> = nonStrict(-1);
|
|
169
|
+
const unsafe = runPipeResult(widened); // any
|
|
180
170
|
|
|
181
|
-
const
|
|
171
|
+
const strict = pipeSideEffectStrict(
|
|
172
|
+
(n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const))
|
|
173
|
+
);
|
|
174
|
+
const precise = strict(-1); // number | SideEffect<'NEG'>
|
|
182
175
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// TypeScript knows: result is number[]
|
|
186
|
-
const sum: number = result.reduce((a, b) => a + b, 0);
|
|
187
|
-
} else {
|
|
188
|
-
// TypeScript knows: result is SideEffect<'No odd numbers'>
|
|
189
|
-
const error = runPipeResult(result); // 'No odd numbers'
|
|
176
|
+
if (isSideEffect(precise)) {
|
|
177
|
+
const err = runPipeResult(precise); // 'NEG'
|
|
190
178
|
}
|
|
191
|
-
|
|
192
|
-
// ⚠️ Non-strict pipeSideEffect widens SideEffect to any
|
|
193
|
-
const widened: number[] | SideEffect<any> = pipeSideEffect(
|
|
194
|
-
(nums: number[]) => nums,
|
|
195
|
-
(nums) => nums.length > 0 ? nums : SideEffect.of(() => 'EMPTY')
|
|
196
|
-
)([]);
|
|
197
|
-
const unsafeValue = runPipeResult(widened); // any
|
|
198
|
-
|
|
199
|
-
// ✅ CORRECT: Provide generics to recover a safe union
|
|
200
|
-
const safeValue = runPipeResult<number[], string>(result); // result: number[] | string (union type - safe but not narrowed)
|
|
201
179
|
```
|
|
202
180
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
`runPipeResult<T, R=any>` has a default type parameter `R=any`. This means:
|
|
206
|
-
|
|
207
|
-
- ✅ **Precise input types**: `T | SideEffect<'E'>` preserves `T | 'E'` without extra annotations.
|
|
208
|
-
- ⚠️ **Widened inputs**: `T | SideEffect<any>` (or `any`) collapses to `any`.
|
|
209
|
-
- ✅ **With generics**: `runPipeResult<SuccessType, ErrorType>(result)` restores a safe union when inference is lost.
|
|
210
|
-
- ✅ **After narrowing**: If the input is `SideEffect<'E'>`, `runPipeResult` returns `'E'`.
|
|
211
|
-
- ✅ **With isSideEffect**: Prefer for runtime narrowing when you need branch-specific types.
|
|
181
|
+
---
|
|
212
182
|
|
|
213
|
-
|
|
183
|
+
## Stream Functions (`fp-pack/stream`) — Lazy Iterables
|
|
214
184
|
|
|
215
|
-
|
|
185
|
+
Use stream utilities when:
|
|
186
|
+
- data is large or unbounded (`range`, streams, generators)
|
|
187
|
+
- you want lazy evaluation (avoid allocating intermediate arrays)
|
|
188
|
+
- you want to support `Iterable` and `AsyncIterable`
|
|
216
189
|
|
|
217
|
-
|
|
190
|
+
Stream utilities are in `fp-pack/stream` and are designed to be pipe-friendly.
|
|
218
191
|
|
|
219
|
-
```
|
|
192
|
+
```ts
|
|
220
193
|
import { pipe } from 'fp-pack';
|
|
221
|
-
import {
|
|
194
|
+
import { range, filter, map, take, toArray } from 'fp-pack/stream';
|
|
222
195
|
|
|
223
|
-
|
|
224
|
-
const processLargeDataset = pipe(
|
|
196
|
+
const first100SquaresOfEvens = pipe(
|
|
225
197
|
filter((n: number) => n % 2 === 0),
|
|
226
|
-
map(n => n * n),
|
|
198
|
+
map((n) => n * n),
|
|
227
199
|
take(100),
|
|
228
200
|
toArray
|
|
229
201
|
);
|
|
230
202
|
|
|
231
|
-
|
|
232
|
-
const result = processLargeDataset(range(1, 1000000));
|
|
233
|
-
|
|
234
|
-
// BAD: Eager array processing
|
|
235
|
-
const result = Array.from({ length: 1000000 }, (_, i) => i + 1)
|
|
236
|
-
.filter(n => n % 2 === 0)
|
|
237
|
-
.map(n => n * n)
|
|
238
|
-
.slice(0, 100); // Processed entire dataset!
|
|
203
|
+
const result = first100SquaresOfEvens(range(Infinity));
|
|
239
204
|
```
|
|
240
205
|
|
|
241
|
-
|
|
242
|
-
- Sync: `Iterable<T>` → `IterableIterator<R>`
|
|
243
|
-
- Async: `AsyncIterable<T>` → `AsyncIterableIterator<R>`
|
|
206
|
+
---
|
|
244
207
|
|
|
245
|
-
## Available Functions
|
|
208
|
+
## Available Functions (Practical Index)
|
|
246
209
|
|
|
247
|
-
|
|
248
|
-
- `pipe` - Left-to-right function composition (sync)
|
|
249
|
-
- `pipeSideEffect` - Left-to-right composition with SideEffect short-circuiting
|
|
250
|
-
- `pipeSideEffectStrict` - SideEffect composition with strict effect unions
|
|
251
|
-
- `compose` - Right-to-left function composition
|
|
252
|
-
- `curry` - Curry a function
|
|
253
|
-
- `partial` - Partial application
|
|
254
|
-
- `flip` - Flip function argument order
|
|
255
|
-
- `complement` - Logical negation
|
|
256
|
-
- `identity` - Return input unchanged
|
|
257
|
-
- `constant` - Always return the same value
|
|
258
|
-
- `from` - Ignore input and return a fixed value
|
|
259
|
-
- `tap` - Execute side effect and return original value
|
|
260
|
-
- `tap0` - Execute side effect without input
|
|
261
|
-
- `once` - Execute function only once
|
|
262
|
-
- `memoize` - Cache function results
|
|
263
|
-
- `SideEffect` - Side effect container
|
|
264
|
-
- `isSideEffect` - Type guard for SideEffect
|
|
265
|
-
- `matchSideEffect` - Pattern match on value/SideEffect
|
|
266
|
-
- `runPipeResult` - Execute SideEffect or return value
|
|
210
|
+
This is not a complete API reference; it’s the “what to reach for” index when composing pipelines.
|
|
267
211
|
|
|
268
|
-
###
|
|
269
|
-
- `pipeAsync`
|
|
270
|
-
-
|
|
271
|
-
- `
|
|
272
|
-
- `
|
|
273
|
-
- `timeout` - Add timeout to promise
|
|
274
|
-
- `retry` - Retry failed operations
|
|
275
|
-
- `debounce` - Debounce function calls
|
|
276
|
-
- `debounceLeading` - Debounce with leading edge
|
|
277
|
-
- `debounceLeadingTrailing` - Debounce with both edges
|
|
278
|
-
- `throttle` - Throttle function calls
|
|
212
|
+
### Composition
|
|
213
|
+
- `pipe`, `pipeAsync`
|
|
214
|
+
- SideEffect-aware: `pipeSideEffect`, `pipeSideEffectStrict`, `pipeAsyncSideEffect`, `pipeAsyncSideEffectStrict`
|
|
215
|
+
- Utilities: `from`, `tap`, `tap0`, `once`, `memoize`, `identity`, `constant`, `flip`, `partial`, `curry`, `compose`, `complement`
|
|
216
|
+
- SideEffect helpers: `SideEffect`, `isSideEffect`, `matchSideEffect`, `runPipeResult`
|
|
279
217
|
|
|
280
218
|
### Array
|
|
281
|
-
- `map`, `filter`, `reduce`, `
|
|
282
|
-
- `find`, `some`, `every`
|
|
283
|
-
- `take`, `drop`, `takeWhile`, `dropWhile`
|
|
284
|
-
-
|
|
285
|
-
-
|
|
286
|
-
- `concat`, `append`, `prepend`, `flatten
|
|
287
|
-
- `head`, `tail`, `last`, `init`
|
|
288
|
-
- `range`, `partition`, `scan`
|
|
219
|
+
- Core transforms: `map`, `filter`, `flatMap`, `reduce`, `scan`
|
|
220
|
+
- Queries: `find`, `some`, `every`
|
|
221
|
+
- Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
|
|
222
|
+
- Ordering/grouping: `sort`, `sortBy`, `groupBy`, `uniqBy`
|
|
223
|
+
- Pairing: `zip`, `zipWith`
|
|
224
|
+
- Combining: `concat`, `append`, `prepend`, `flatten`
|
|
289
225
|
|
|
290
226
|
### Object
|
|
291
|
-
- `prop`, `propOr`, `path`, `pathOr`
|
|
292
|
-
- `pick`, `omit`
|
|
293
|
-
- `assoc`, `assocPath`, `
|
|
294
|
-
- `merge`, `mergeDeep
|
|
295
|
-
-
|
|
296
|
-
-
|
|
297
|
-
- `has`, `hasPath`
|
|
227
|
+
- Access: `prop`, `propOr`, `propStrict`, `path`, `pathOr`
|
|
228
|
+
- Pick/drop: `pick`, `omit`
|
|
229
|
+
- Updates: `assoc`, `assocPath`, `dissocPath`
|
|
230
|
+
- Merge: `merge`, `mergeDeep`
|
|
231
|
+
- Transforms: `mapValues`, `evolve`
|
|
232
|
+
- Predicates: `has`
|
|
298
233
|
|
|
299
234
|
### Control Flow
|
|
300
|
-
- `ifElse`
|
|
301
|
-
- `when`, `unless` - Conditional execution
|
|
302
|
-
- `cond` - Multi-branch conditional
|
|
303
|
-
- `tryCatch` - Safe function execution
|
|
304
|
-
- `guard` - Validation guard
|
|
305
|
-
|
|
306
|
-
### Stream (Lazy Iterables)
|
|
307
|
-
- `append`, `concat`, `prepend`
|
|
308
|
-
- `map`, `filter`, `flatMap`, `flatten`, `flattenDeep`
|
|
309
|
-
- `take`, `takeWhile`, `drop`, `dropWhile`, `chunk`
|
|
310
|
-
- `zip`, `zipWith`, `find`, `some`, `every`
|
|
311
|
-
- `reduce`, `scan`
|
|
312
|
-
- `range`
|
|
313
|
-
- `toArray` - Materialize stream to array
|
|
314
|
-
- `toAsync` - Convert to async iterable
|
|
315
|
-
|
|
316
|
-
### Math
|
|
317
|
-
- `add`, `sub`, `mul`, `div`
|
|
318
|
-
- `sum`, `mean`, `min`, `max`
|
|
319
|
-
- `round`, `floor`, `ceil`, `randomInt`
|
|
320
|
-
|
|
321
|
-
### String
|
|
322
|
-
- `trim`, `split`, `join`, `replace`
|
|
323
|
-
- `toUpper`, `toLower`
|
|
324
|
-
- `startsWith`, `endsWith`, `match`
|
|
325
|
-
|
|
326
|
-
### Equality
|
|
327
|
-
- `equals`, `includes`
|
|
328
|
-
- `isNil`, `isEmpty`, `isType`
|
|
329
|
-
- `gt`, `gte`, `lt`, `lte`
|
|
330
|
-
- `clamp`
|
|
331
|
-
|
|
332
|
-
### Nullable
|
|
333
|
-
- `maybe`, `mapMaybe`, `getOrElse`, `fold`, `result`
|
|
334
|
-
|
|
335
|
-
### Debug
|
|
336
|
-
- `assert`, `invariant`, `log`
|
|
235
|
+
- `ifElse`, `when`, `unless`, `cond`, `guard`, `tryCatch`
|
|
337
236
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
Before writing code, check whether the project is JavaScript or TypeScript:
|
|
343
|
-
|
|
344
|
-
- **TypeScript projects**: use explicit types, leverage generics, and keep type-safe signatures in examples.
|
|
345
|
-
- **JavaScript projects**: avoid TypeScript-only syntax and prefer JSDoc only when it adds clarity or is already used.
|
|
346
|
-
|
|
347
|
-
### 0.1 Quick Signature Lookup
|
|
348
|
-
|
|
349
|
-
If a function signature or argument order is unclear, check the local declaration or source files:
|
|
350
|
-
|
|
351
|
-
- Main exports: `dist/index.d.ts`
|
|
352
|
-
- Stream exports: `dist/stream/index.d.ts`
|
|
353
|
-
- Main utilities (fallback): `src/implement/**`
|
|
354
|
-
- Stream utilities (fallback): `src/stream/**`
|
|
355
|
-
- Installed package:
|
|
356
|
-
- `node_modules/fp-pack/dist/index.d.ts`
|
|
357
|
-
- `node_modules/fp-pack/dist/stream/index.d.ts`
|
|
358
|
-
- `node_modules/fp-pack/src/implement/**`
|
|
359
|
-
- `node_modules/fp-pack/src/stream/**`
|
|
360
|
-
|
|
361
|
-
### 1. Always Prefer pipe/pipeAsync
|
|
362
|
-
|
|
363
|
-
```typescript
|
|
364
|
-
// GOOD
|
|
365
|
-
const result = pipe(
|
|
366
|
-
trim,
|
|
367
|
-
split(','),
|
|
368
|
-
map(toNumber),
|
|
369
|
-
filter(isPositive)
|
|
370
|
-
)(input);
|
|
371
|
-
|
|
372
|
-
// BAD
|
|
373
|
-
const trimmed = trim(input);
|
|
374
|
-
const parts = split(',')(trimmed);
|
|
375
|
-
const numbers = map(toNumber)(parts);
|
|
376
|
-
const result = filter(isPositive)(numbers);
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### 2. Use Curried Functions (Where Available)
|
|
380
|
-
|
|
381
|
-
Most multi-arg functions are curried. Single-arg utilities are already unary, so currying adds no benefit; use them directly (e.g. `uniq`, `flatten`, `flattenDeep`, `head`, `tail`, `last`, `init`, `range`, `sum`, `mean`, `min`, `max`, `round`, `floor`, `ceil`, `trim`, `toLower`, `toUpper`, `isNil`, `isEmpty`, `isType`). These are already pipe-friendly without a curried variant.
|
|
237
|
+
### Async
|
|
238
|
+
- `retry`, `timeout`, `delay`
|
|
239
|
+
- Function-returning helpers: `debounce*`, `throttle`
|
|
382
240
|
|
|
383
|
-
|
|
384
|
-
|
|
241
|
+
### Stream (Lazy Iterables)
|
|
242
|
+
- Building: `range`
|
|
243
|
+
- Transforms: `map`, `filter`, `flatMap`, `flatten`
|
|
244
|
+
- Slicing: `take`, `drop`, `takeWhile`, `dropWhile`, `chunk`
|
|
245
|
+
- Queries/reductions: `find`, `some`, `every`, `reduce`, `scan`
|
|
246
|
+
- Pairing/combining: `zip`, `zipWith`, `concat`, `append`, `prepend`
|
|
247
|
+
- Utilities: `toArray`, `toAsync`
|
|
248
|
+
|
|
249
|
+
### Math / String / Equality / Debug
|
|
250
|
+
- Math: `add`, `sub`, `mul`, `div`, `randomInt`, `clamp`
|
|
251
|
+
- String: `split`, `join`, `replace`, `match`, `trim`
|
|
252
|
+
- Equality: `equals`, `isNil`
|
|
253
|
+
- Debug: `assert`, `invariant`
|
|
385
254
|
|
|
386
|
-
|
|
387
|
-
const processUsers = pipe(
|
|
388
|
-
filter(user => user.active),
|
|
389
|
-
map(user => user.name)
|
|
390
|
-
);
|
|
255
|
+
---
|
|
391
256
|
|
|
392
|
-
|
|
393
|
-
const filterActive = filter((user: User) => user.active);
|
|
394
|
-
const getNames = map((user: User) => user.name);
|
|
395
|
-
const processUsers = pipe(filterActive, getNames);
|
|
396
|
-
```
|
|
257
|
+
## Micro-Patterns (Keep It Short)
|
|
397
258
|
|
|
398
|
-
###
|
|
259
|
+
### Boundary handling (UI/event/service)
|
|
399
260
|
|
|
400
|
-
|
|
261
|
+
Do the work in a pipeline, unwrap at the boundary:
|
|
401
262
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
- **Fixed signatures** can use `curry(fn)` directly.
|
|
405
|
-
- **Generic or overloaded signatures** should use an explicit type alias + cast to preserve inference.
|
|
263
|
+
```ts
|
|
264
|
+
import { pipeSideEffectStrict, SideEffect, isSideEffect, runPipeResult } from 'fp-pack';
|
|
406
265
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
function split(separator: string, str: string): string[] {
|
|
410
|
-
return str.split(separator);
|
|
411
|
-
}
|
|
412
|
-
export default curry(split);
|
|
266
|
+
const validate = (n: number) => (n > 0 ? n : SideEffect.of(() => 'NEG' as const));
|
|
267
|
+
const pipeline = pipeSideEffectStrict(validate, (n) => n + 1);
|
|
413
268
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
(
|
|
417
|
-
|
|
269
|
+
export const handler = (n: number) => {
|
|
270
|
+
const result = pipeline(n);
|
|
271
|
+
if (isSideEffect(result)) return runPipeResult(result); // 'NEG'
|
|
272
|
+
return result; // number
|
|
418
273
|
};
|
|
419
|
-
|
|
420
|
-
function chunk<T>(size: number, arr: T[]): T[][] {
|
|
421
|
-
// ...
|
|
422
|
-
return [];
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const curriedChunk = curry(chunk) as Chunk;
|
|
426
|
-
export default curriedChunk;
|
|
427
274
|
```
|
|
428
275
|
|
|
429
|
-
###
|
|
430
|
-
|
|
431
|
-
**Default choice: Start with `pipe` / `pipeAsync`**
|
|
432
|
-
|
|
433
|
-
Most data transformations are pure and don't need SideEffect handling. Use `pipe` for sync operations and `pipeAsync` for async operations. **Only switch to SideEffect-aware pipes when you actually need** early termination or error handling with side effects.
|
|
276
|
+
### Data-first style with `from()`
|
|
434
277
|
|
|
435
|
-
-
|
|
436
|
-
- **`pipeAsync`** - Async, **pure** transformations (99% of cases)
|
|
437
|
-
- **`pipeSideEffect`** - **Only when you need** SideEffect short-circuiting (sync)
|
|
438
|
-
- **`pipeAsyncSideEffect`** - **Only when you need** SideEffect short-circuiting (async)
|
|
439
|
-
- **`pipeSideEffectStrict`** - Sync SideEffect pipelines with strict effect unions
|
|
440
|
-
- **`pipeAsyncSideEffectStrict`** - Async SideEffect pipelines with strict effect unions
|
|
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()`:
|
|
441
279
|
|
|
442
|
-
|
|
280
|
+
```ts
|
|
281
|
+
import { pipe, from, map, filter, take } from 'fp-pack';
|
|
443
282
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
)
|
|
283
|
+
// data-first feeling: start with data, then compose transforms
|
|
284
|
+
const result = pipe(
|
|
285
|
+
from([1, 2, 3, 4, 5]),
|
|
286
|
+
filter((n: number) => n % 2 === 0),
|
|
287
|
+
map((n) => n * 10),
|
|
288
|
+
take(2)
|
|
289
|
+
)(); // [20, 40]
|
|
450
290
|
|
|
451
|
-
//
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
map(
|
|
455
|
-
filter(profile => profile.verified)
|
|
291
|
+
// same idea when you already have an input (normal usage)
|
|
292
|
+
const process = pipe(
|
|
293
|
+
filter((n: number) => n % 2 === 0),
|
|
294
|
+
map((n) => n * 10)
|
|
456
295
|
);
|
|
296
|
+
const result2 = process([1, 2, 3, 4, 5]); // [20, 40]
|
|
457
297
|
```
|
|
458
298
|
|
|
459
|
-
###
|
|
460
|
-
|
|
461
|
-
**🔄 Critical Rule: SideEffect Contagion**
|
|
462
|
-
|
|
463
|
-
Once you use `pipeSideEffect` or `pipeAsyncSideEffect`, the result is **always `T | SideEffect`** (or `Promise<T | SideEffect>` for async). The same rule applies to strict variants.
|
|
464
|
-
|
|
465
|
-
If you want to continue composing this result, you **MUST** keep using SideEffect-aware pipes. You **CANNOT** switch back to `pipe` or `pipeAsync` because they don't handle `SideEffect`.
|
|
299
|
+
### `ifElse` uses functions (not values)
|
|
466
300
|
|
|
467
|
-
```
|
|
468
|
-
import {
|
|
301
|
+
```ts
|
|
302
|
+
import { ifElse, from } from 'fp-pack';
|
|
469
303
|
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
)
|
|
474
|
-
// Result type: User | SideEffect
|
|
475
|
-
|
|
476
|
-
// ❌ WRONG - pipe cannot handle SideEffect
|
|
477
|
-
const wrongPipeline = pipe(
|
|
478
|
-
validateUserPipeline, // Returns User | SideEffect
|
|
479
|
-
(user) => user.email // Type error! SideEffect has no 'email' property
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
// ✅ CORRECT - Keep using pipeSideEffect
|
|
483
|
-
const correctPipeline = pipeSideEffect(
|
|
484
|
-
validateUserPipeline, // User | SideEffect - handled correctly
|
|
485
|
-
(user) => user.email, // Automatically skipped if SideEffect
|
|
486
|
-
sendEmail
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
// The same rule applies to async pipes
|
|
490
|
-
const asyncPipeline = pipeAsyncSideEffect(
|
|
491
|
-
fetchUser,
|
|
492
|
-
validateUser
|
|
493
|
-
);
|
|
494
|
-
// Result type: Promise<User | SideEffect>
|
|
495
|
-
|
|
496
|
-
// You must continue with pipeAsyncSideEffect, not pipeAsync
|
|
497
|
-
const extendedAsyncPipeline = pipeAsyncSideEffect(
|
|
498
|
-
asyncPipeline,
|
|
499
|
-
processUser,
|
|
500
|
-
saveToDatabase
|
|
304
|
+
const label = ifElse(
|
|
305
|
+
(n: number) => n >= 60,
|
|
306
|
+
from('pass'),
|
|
307
|
+
from('fail')
|
|
501
308
|
);
|
|
502
309
|
```
|
|
503
310
|
|
|
504
|
-
###
|
|
311
|
+
### Stream pipeline to array
|
|
505
312
|
|
|
506
|
-
```
|
|
313
|
+
```ts
|
|
507
314
|
import { pipe } from 'fp-pack';
|
|
508
|
-
import {
|
|
315
|
+
import { map, filter, toArray } from 'fp-pack/stream';
|
|
509
316
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
take(100),
|
|
317
|
+
const toIds = pipe(
|
|
318
|
+
filter((u: User) => u.active),
|
|
319
|
+
map((u) => u.id),
|
|
514
320
|
toArray
|
|
515
321
|
);
|
|
516
|
-
|
|
517
|
-
// Stops after finding 100 items (only processes 100, not 1 million)
|
|
518
|
-
const result = getFirst100Even(range(1, 1000000));
|
|
519
322
|
```
|
|
520
323
|
|
|
521
|
-
###
|
|
522
|
-
|
|
523
|
-
```typescript
|
|
524
|
-
import { pipeSideEffect, SideEffect, runPipeResult } from 'fp-pack';
|
|
525
|
-
|
|
526
|
-
const safeDividePipeline = pipeSideEffect(
|
|
527
|
-
(input: { a: number; b: number }) => {
|
|
528
|
-
if (input.b === 0) {
|
|
529
|
-
return SideEffect.of(() => {
|
|
530
|
-
throw new Error('Division by zero');
|
|
531
|
-
}, 'DIVISION_ERROR');
|
|
532
|
-
}
|
|
533
|
-
return input;
|
|
534
|
-
},
|
|
535
|
-
({ a, b }) => a / b
|
|
536
|
-
);
|
|
324
|
+
### Data-last inference workaround (when needed)
|
|
537
325
|
|
|
538
|
-
|
|
539
|
-
const result = runPipeResult(safeDividePipeline({ a: 10, b: 2 })); // 5
|
|
540
|
-
```
|
|
326
|
+
Use a wrapper or `pipeHint` when a data-last generic won’t infer inside `pipe`:
|
|
541
327
|
|
|
542
|
-
|
|
328
|
+
```ts
|
|
329
|
+
import { pipe, pipeHint, zip } from 'fp-pack';
|
|
543
330
|
|
|
544
|
-
|
|
545
|
-
|
|
331
|
+
// wrapper
|
|
332
|
+
const zipUsers = pipe((xs: User[]) => zip(['a', 'b', 'c'], xs));
|
|
546
333
|
|
|
547
|
-
//
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
(age: number) => age >= 18,
|
|
551
|
-
age => ({ age, status: 'adult' }),
|
|
552
|
-
age => ({ age, status: 'minor' })
|
|
553
|
-
)
|
|
334
|
+
// pipeHint
|
|
335
|
+
const zipUsers2 = pipe(
|
|
336
|
+
pipeHint<User[], Array<[string, User]>>(zip(['a', 'b', 'c']))
|
|
554
337
|
);
|
|
555
|
-
|
|
556
|
-
// GOOD: Multi-branch with cond
|
|
557
|
-
const gradeToLetter = cond([
|
|
558
|
-
[(n: number) => n >= 90, () => 'A'],
|
|
559
|
-
[(n: number) => n >= 80, () => 'B'],
|
|
560
|
-
[(n: number) => n >= 70, () => 'C'],
|
|
561
|
-
[() => true, () => 'F']
|
|
562
|
-
]);
|
|
563
338
|
```
|
|
564
339
|
|
|
565
|
-
###
|
|
566
|
-
|
|
567
|
-
- `prop` returns `T[K] | undefined`. Use `propOr` (or guard) before array operations.
|
|
568
|
-
- `ifElse` expects **functions** for both branches. If you already have a value, wrap it: `() => value` or use `from(value)` for cleaner constant branches.
|
|
569
|
-
- Use `from(value)` when you need a unary function that ignores input (handy for `ifElse`/`cond` branches and data-first patterns). Pipelines that start with `from(...)` can be called without an initial input value.
|
|
570
|
-
- `cond` returns `R | undefined`. Add a default branch and coalesce when you need a strict result.
|
|
571
|
-
- In `pipeSideEffect`, keep step return types aligned to avoid wide unions.
|
|
572
|
-
|
|
573
|
-
```typescript
|
|
574
|
-
import { pipe, propOr, append, assoc, ifElse, cond, from, filter, map } from 'fp-pack';
|
|
575
|
-
|
|
576
|
-
// propOr keeps the type strict for array ops
|
|
577
|
-
const addTodo = (text: string, state: AppState) =>
|
|
578
|
-
pipe(
|
|
579
|
-
propOr([], 'todos'),
|
|
580
|
-
append(createTodo(text)),
|
|
581
|
-
(todos) => assoc('todos', todos, state)
|
|
582
|
-
)(state);
|
|
583
|
-
|
|
584
|
-
// ifElse expects functions, not values
|
|
585
|
-
const toggleTodo = (id: string) => ifElse(
|
|
586
|
-
(todo: Todo) => todo.id === id,
|
|
587
|
-
assoc('completed', true),
|
|
588
|
-
(todo) => todo
|
|
589
|
-
);
|
|
590
|
-
|
|
591
|
-
// from is useful for constant branches - cleaner than () => value
|
|
592
|
-
const statusLabel = ifElse(
|
|
593
|
-
(score: number) => score >= 60,
|
|
594
|
-
from('pass'), // Constant value
|
|
595
|
-
from('fail')
|
|
596
|
-
);
|
|
597
|
-
|
|
598
|
-
// Data-first pattern with from: inject data into pipeline
|
|
599
|
-
const processData = pipe(
|
|
600
|
-
from([1, 2, 3, 4, 5]),
|
|
601
|
-
filter((n: number) => n % 2 === 0),
|
|
602
|
-
map(n => n * 2)
|
|
603
|
-
);
|
|
604
|
-
const result = processData(); // [4, 8]
|
|
605
|
-
|
|
606
|
-
// cond still returns R | undefined, so coalesce if needed
|
|
607
|
-
const grade = (score: number) =>
|
|
608
|
-
cond([
|
|
609
|
-
[(n: number) => n >= 90, () => 'A'],
|
|
610
|
-
[() => true, () => 'F']
|
|
611
|
-
])(score) ?? 'F';
|
|
612
|
-
```
|
|
340
|
+
### Async pipelines (retry/timeout)
|
|
613
341
|
|
|
614
|
-
|
|
342
|
+
Keep async steps inside `pipeAsync` and push configuration (like `timeout`) via currying:
|
|
615
343
|
|
|
616
|
-
```
|
|
617
|
-
import {
|
|
344
|
+
```ts
|
|
345
|
+
import { pipeAsync, retry, timeout } from 'fp-pack';
|
|
618
346
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
mapValues((value) => typeof value === 'string' ? value.trim() : value),
|
|
623
|
-
merge({ verified: false })
|
|
347
|
+
const fetchJson = pipeAsync(
|
|
348
|
+
(url: string) => fetch(url),
|
|
349
|
+
(res) => res.json()
|
|
624
350
|
);
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
## Anti-Patterns to Avoid
|
|
628
|
-
|
|
629
|
-
### ❌ Don't use imperative loops
|
|
630
351
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
result.push(item.name);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// GOOD
|
|
641
|
-
const result = pipe(
|
|
642
|
-
filter((item: Item) => item.active),
|
|
643
|
-
map(item => item.name)
|
|
644
|
-
)(items);
|
|
352
|
+
const fetchJsonWithGuards = pipeAsync(
|
|
353
|
+
fetchJson,
|
|
354
|
+
timeout(5_000),
|
|
355
|
+
retry(3)
|
|
356
|
+
);
|
|
645
357
|
```
|
|
646
358
|
|
|
647
|
-
###
|
|
359
|
+
### Object updates (avoid mutation)
|
|
648
360
|
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
const result = users
|
|
652
|
-
.filter(u => u.active)
|
|
653
|
-
.map(u => u.name)
|
|
654
|
-
.slice(0, 10);
|
|
361
|
+
```ts
|
|
362
|
+
import { pipe, assocPath, merge } from 'fp-pack';
|
|
655
363
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
map(u => u.name),
|
|
660
|
-
take(10)
|
|
661
|
-
)(users);
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
### ❌ Don't use traditional monads (Option, Either, Maybe)
|
|
665
|
-
|
|
666
|
-
```typescript
|
|
667
|
-
// BAD - Don't implement this pattern
|
|
668
|
-
const maybeUser = Option.of(user)
|
|
669
|
-
.map(u => u.profile)
|
|
670
|
-
.flatMap(p => p.email);
|
|
671
|
-
|
|
672
|
-
// GOOD - Use SideEffect with pipeSideEffect
|
|
673
|
-
const getUserEmail = pipeSideEffect(
|
|
674
|
-
(user: User) => {
|
|
675
|
-
if (!user.profile) {
|
|
676
|
-
return SideEffect.of(() => null, 'NO_PROFILE');
|
|
677
|
-
}
|
|
678
|
-
return user.profile;
|
|
679
|
-
},
|
|
680
|
-
(profile) => {
|
|
681
|
-
if (!profile.email) {
|
|
682
|
-
return SideEffect.of(() => null, 'NO_EMAIL');
|
|
683
|
-
}
|
|
684
|
-
return profile.email;
|
|
685
|
-
}
|
|
364
|
+
const updateAccount = pipe(
|
|
365
|
+
assocPath(['profile', 'role'], 'member'),
|
|
366
|
+
merge({ updatedAt: Date.now() })
|
|
686
367
|
);
|
|
687
368
|
```
|
|
688
369
|
|
|
689
|
-
|
|
370
|
+
---
|
|
690
371
|
|
|
691
|
-
|
|
692
|
-
// BAD
|
|
693
|
-
const updateUser = (user: User) => {
|
|
694
|
-
user.lastLogin = new Date();
|
|
695
|
-
return user;
|
|
696
|
-
};
|
|
372
|
+
## Decision Guide (What Should the Agent Pick?)
|
|
697
373
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
374
|
+
- Is everything sync and pure? → `pipe`
|
|
375
|
+
- Any step async? → `pipeAsync`
|
|
376
|
+
- Need early-exit + typed effect unions? → `pipeSideEffectStrict` / `pipeAsyncSideEffectStrict`
|
|
377
|
+
- Need early-exit but type precision doesn’t matter? → `pipeSideEffect` / `pipeAsyncSideEffect`
|
|
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)
|
|
381
|
+
- Large/unbounded/iterable data? → `fp-pack/stream`
|
|
703
382
|
|
|
704
|
-
|
|
705
|
-
import { assoc } from 'fp-pack';
|
|
706
|
-
const updateUser = assoc('lastLogin', new Date());
|
|
707
|
-
```
|
|
383
|
+
---
|
|
708
384
|
|
|
709
385
|
## Quick Reference
|
|
710
386
|
|
|
711
387
|
### Import Paths
|
|
712
|
-
|
|
713
|
-
-
|
|
714
|
-
- SideEffect: `import { pipeSideEffect, pipeSideEffectStrict,
|
|
388
|
+
|
|
389
|
+
- Main: `import { pipe, map, filter } from 'fp-pack'`
|
|
390
|
+
- SideEffect pipes: `import { pipeSideEffect, pipeSideEffectStrict, SideEffect } from 'fp-pack'`
|
|
391
|
+
- Async: `import { pipeAsync, retry, timeout } from 'fp-pack'`
|
|
715
392
|
- Stream: `import { map, filter, toArray } from 'fp-pack/stream'`
|
|
716
393
|
|
|
717
394
|
### When to Use What
|
|
718
|
-
- **Pure sync transformations**: `pipe` + array/object functions
|
|
719
|
-
- **Pure async operations**: `pipeAsync`
|
|
720
|
-
- **Error handling with SideEffect**: `pipeSideEffect` (sync) / `pipeAsyncSideEffect` (async)
|
|
721
|
-
- **Strict SideEffect unions**: `pipeSideEffectStrict` (sync) / `pipeAsyncSideEffectStrict` (async)
|
|
722
|
-
- **Type-safe result handling**: `isSideEffect` for precise type narrowing (prefer this when you need branch-specific types)
|
|
723
|
-
- **Execute SideEffect**: `runPipeResult` (call OUTSIDE pipelines). If the input is narrowed to `SideEffect<R>`, it returns `R`. If the input is widened to `SideEffect<any>`/`any`, the result becomes `any`; provide generics to recover
|
|
724
|
-
- **Large datasets**: `stream/*` functions
|
|
725
|
-
- **Conditionals**: `ifElse`, `when`, `unless`, `cond`
|
|
726
|
-
- **Object access**: `prop`, `propStrict`, `path`, `pick`, `omit`
|
|
727
|
-
- **Object updates**: `assoc`, `merge`, `evolve`
|
|
728
|
-
|
|
729
|
-
## UI Framework Integration Patterns
|
|
730
|
-
|
|
731
|
-
fp-pack works seamlessly with UI frameworks. Here are common patterns organized by **use case**, not framework.
|
|
732
|
-
|
|
733
|
-
### Pattern 1: Handling User Input
|
|
734
|
-
|
|
735
|
-
**When**: Form inputs, button clicks, drag & drop, any user interaction
|
|
736
|
-
**Where to use**: Event handlers (onChange, @input, on:click, etc.)
|
|
737
|
-
|
|
738
|
-
```typescript
|
|
739
|
-
import { pipe, pipeAsyncSideEffect, trim, prop, assoc, tap, SideEffect, runPipeResult } from 'fp-pack';
|
|
740
|
-
|
|
741
|
-
// GOOD: Process form input declaratively
|
|
742
|
-
const handleNameChange = pipe(
|
|
743
|
-
prop('currentTarget'), // Safer than target in most UI libs
|
|
744
|
-
(el) => (el as HTMLInputElement).value,
|
|
745
|
-
trim,
|
|
746
|
-
tap((value) => {
|
|
747
|
-
// Prefer updater form to avoid stale state in React-like frameworks
|
|
748
|
-
setFormState(prev => assoc('name', value, prev));
|
|
749
|
-
})
|
|
750
|
-
);
|
|
751
395
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
const errors = validateFields(data);
|
|
760
|
-
if (!errors) return data;
|
|
761
|
-
return SideEffect.of(() => {
|
|
762
|
-
setErrors(errors);
|
|
763
|
-
return null;
|
|
764
|
-
}, 'VALIDATION_ERROR');
|
|
765
|
-
};
|
|
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`
|
|
766
403
|
|
|
767
|
-
|
|
768
|
-
tap((e: Event) => e.preventDefault()),
|
|
769
|
-
prop('currentTarget'),
|
|
770
|
-
(form) => getFormData(form as HTMLFormElement),
|
|
771
|
-
validateFieldsOrStop, // Returns data or SideEffect
|
|
772
|
-
sanitizeInput,
|
|
773
|
-
submitToAPI
|
|
774
|
-
);
|
|
775
|
-
|
|
776
|
-
const handleSubmit = (e: Event) => runPipeResult(handleSubmitPipeline(e));
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
### Pattern 2: Computing Derived/Reactive Values
|
|
780
|
-
|
|
781
|
-
**When**: Displaying filtered/sorted/transformed data from state
|
|
782
|
-
**Where to use**: Computed properties, memoized values, derived state
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
import { pipe, filter, sortBy, map, take } from 'fp-pack';
|
|
786
|
-
|
|
787
|
-
// GOOD: Create reusable data transformation
|
|
788
|
-
const processUsers = pipe(
|
|
789
|
-
filter((u: User) => u.status === 'active'),
|
|
790
|
-
sortBy(u => u.lastLogin),
|
|
791
|
-
map(u => ({ ...u, displayName: `${u.firstName} ${u.lastName}` })),
|
|
792
|
-
take(50)
|
|
793
|
-
);
|
|
794
|
-
|
|
795
|
-
// Use in any framework:
|
|
796
|
-
// React: const processed = useMemo(() => processUsers(users), [users]);
|
|
797
|
-
// Vue: const processed = computed(() => processUsers(users.value));
|
|
798
|
-
// Svelte: $: processed = processUsers($users);
|
|
799
|
-
// Solid: const processed = createMemo(() => processUsers(users()));
|
|
800
|
-
|
|
801
|
-
// GOOD: Search + filter + pagination
|
|
802
|
-
const searchUsers = (query: string, page: number) =>
|
|
803
|
-
pipe(
|
|
804
|
-
filter((u: User) =>
|
|
805
|
-
u.name.toLowerCase().includes(query.toLowerCase())
|
|
806
|
-
),
|
|
807
|
-
sortBy(u => u.name),
|
|
808
|
-
chunk(20), // Paginate
|
|
809
|
-
(pages) => pages[page] || []
|
|
810
|
-
);
|
|
811
|
-
|
|
812
|
-
// React example:
|
|
813
|
-
// const results = useMemo(
|
|
814
|
-
// () => searchUsers(searchQuery, currentPage)(allUsers),
|
|
815
|
-
// [searchQuery, currentPage, allUsers]
|
|
816
|
-
// );
|
|
817
|
-
```
|
|
818
|
-
|
|
819
|
-
### Pattern 3: Async Data Fetching and Processing
|
|
820
|
-
|
|
821
|
-
**When**: API calls, database queries, file operations
|
|
822
|
-
**Where to use**: Lifecycle hooks, effects, async event handlers
|
|
823
|
-
|
|
824
|
-
```typescript
|
|
825
|
-
import { pipeAsync, pipeAsyncSideEffect, tap, SideEffect, runPipeResult } from 'fp-pack';
|
|
826
|
-
import { filter, map } from 'fp-pack';
|
|
827
|
-
|
|
828
|
-
// GOOD: Fetch + transform + update state
|
|
829
|
-
const fetchAndProcessUsers = pipeAsync(
|
|
830
|
-
async (userId: string) => fetch(`/api/users/${userId}/friends`),
|
|
831
|
-
async (res) => res.json(),
|
|
832
|
-
filter((u: User) => u.isActive),
|
|
833
|
-
map(u => ({ id: u.id, name: u.name, avatar: u.avatar })),
|
|
834
|
-
(processed) => {
|
|
835
|
-
setUsers(processed); // Framework-specific state update
|
|
836
|
-
return processed;
|
|
837
|
-
}
|
|
838
|
-
);
|
|
839
|
-
|
|
840
|
-
// Use in any framework:
|
|
841
|
-
// React: useEffect(() => { fetchAndProcessUsers(id); }, [id]);
|
|
842
|
-
// Vue: watchEffect(() => fetchAndProcessUsers(userId.value));
|
|
843
|
-
// Svelte: $: fetchAndProcessUsers($userId);
|
|
844
|
-
|
|
845
|
-
// GOOD: Error handling with SideEffect
|
|
846
|
-
const validateResponseOrStop = (users: unknown) => {
|
|
847
|
-
if (!Array.isArray(users)) {
|
|
848
|
-
return SideEffect.of(() => {
|
|
849
|
-
setError('Invalid response');
|
|
850
|
-
return [];
|
|
851
|
-
}, 'INVALID_RESPONSE');
|
|
852
|
-
}
|
|
853
|
-
return users as User[];
|
|
854
|
-
};
|
|
855
|
-
|
|
856
|
-
const safeFetchUsersPipeline = pipeAsyncSideEffect(
|
|
857
|
-
fetchUsers,
|
|
858
|
-
validateResponseOrStop,
|
|
859
|
-
filter((u: User) => u.verified),
|
|
860
|
-
tap((users) => setUsers(users))
|
|
861
|
-
);
|
|
862
|
-
|
|
863
|
-
const safeFetchUsers = () => runPipeResult(safeFetchUsersPipeline());
|
|
864
|
-
```
|
|
865
|
-
|
|
866
|
-
### Pattern 4: List/Table Data Processing
|
|
867
|
-
|
|
868
|
-
**When**: Displaying lists, tables, grids with search/filter/sort
|
|
869
|
-
**Where to use**: Component render logic, computed values
|
|
870
|
-
|
|
871
|
-
```typescript
|
|
872
|
-
import { pipe, filter, sortBy, groupBy, map } from 'fp-pack';
|
|
873
|
-
|
|
874
|
-
// GOOD: Complete table data pipeline
|
|
875
|
-
const processTableData = (
|
|
876
|
-
data: Product[],
|
|
877
|
-
filters: Filters,
|
|
878
|
-
sortConfig: SortConfig
|
|
879
|
-
) => pipe(
|
|
880
|
-
// Apply filters
|
|
881
|
-
filter((p: Product) => {
|
|
882
|
-
if (filters.category && p.category !== filters.category) return false;
|
|
883
|
-
if (filters.minPrice && p.price < filters.minPrice) return false;
|
|
884
|
-
if (filters.maxPrice && p.price > filters.maxPrice) return false;
|
|
885
|
-
return true;
|
|
886
|
-
}),
|
|
887
|
-
// Apply sorting
|
|
888
|
-
sortBy(sortConfig.direction === 'asc'
|
|
889
|
-
? (p) => p[sortConfig.key]
|
|
890
|
-
: (p) => -p[sortConfig.key]
|
|
891
|
-
),
|
|
892
|
-
// Add row metadata
|
|
893
|
-
map((product, index) => ({
|
|
894
|
-
...product,
|
|
895
|
-
rowId: `row-${index}`,
|
|
896
|
-
isEven: index % 2 === 0
|
|
897
|
-
}))
|
|
898
|
-
)(data);
|
|
899
|
-
|
|
900
|
-
// GOOD: Group for categorized display
|
|
901
|
-
const groupProductsByCategory = pipe(
|
|
902
|
-
groupBy((p: Product) => p.category),
|
|
903
|
-
(grouped) => Object.entries(grouped).map(([category, products]) => ({
|
|
904
|
-
category,
|
|
905
|
-
products,
|
|
906
|
-
count: products.length,
|
|
907
|
-
totalValue: products.reduce((sum, p) => sum + p.price, 0)
|
|
908
|
-
}))
|
|
909
|
-
);
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
### Pattern 5: Form State Management
|
|
913
|
-
|
|
914
|
-
**When**: Complex forms with validation and state
|
|
915
|
-
**Where to use**: Form submission, field updates, validation
|
|
916
|
-
|
|
917
|
-
```typescript
|
|
918
|
-
import { pipe, pipeSideEffect, assoc, pick, mapValues, SideEffect, runPipeResult } from 'fp-pack';
|
|
919
|
-
|
|
920
|
-
// GOOD: Update nested form state immutably
|
|
921
|
-
const updateField = (fieldName: string, value: any) =>
|
|
922
|
-
pipe(
|
|
923
|
-
assoc(fieldName, value),
|
|
924
|
-
(state) => assoc('touched', { ...state.touched, [fieldName]: true }, state)
|
|
925
|
-
);
|
|
926
|
-
|
|
927
|
-
// GOOD: Form submission with validation
|
|
928
|
-
const validateFormOrStop = (data: any) => {
|
|
929
|
-
const errors = validateFormData(data);
|
|
930
|
-
return Object.keys(errors).length > 0
|
|
931
|
-
? SideEffect.of(() => {
|
|
932
|
-
setFormErrors(errors);
|
|
933
|
-
return null;
|
|
934
|
-
}, 'VALIDATION_ERROR')
|
|
935
|
-
: data;
|
|
936
|
-
};
|
|
937
|
-
|
|
938
|
-
const submitFormPipeline = pipeSideEffect(
|
|
939
|
-
pick(['email', 'password', 'name']), // Only include relevant fields
|
|
940
|
-
mapValues((v) => typeof v === 'string' ? v.trim() : v), // Sanitize
|
|
941
|
-
validateFormOrStop,
|
|
942
|
-
submitToAPI
|
|
943
|
-
);
|
|
944
|
-
|
|
945
|
-
const submitForm = (data: any) => runPipeResult(submitFormPipeline(data));
|
|
946
|
-
|
|
947
|
-
// GOOD: Multi-step form state
|
|
948
|
-
const validateCurrentStepOrStop = (state: any) => {
|
|
949
|
-
const errors = validateCurrentStep(state);
|
|
950
|
-
if (!errors) return state;
|
|
951
|
-
return SideEffect.of(() => {
|
|
952
|
-
setStepErrors(errors);
|
|
953
|
-
return state;
|
|
954
|
-
}, 'STEP_VALIDATION_ERROR');
|
|
955
|
-
};
|
|
956
|
-
|
|
957
|
-
const goToNextStepPipeline = pipeSideEffect(
|
|
958
|
-
validateCurrentStepOrStop,
|
|
959
|
-
(state) => assoc('currentStep', state.currentStep + 1, state)
|
|
960
|
-
);
|
|
961
|
-
|
|
962
|
-
const goToNextStep = (state: any) => runPipeResult(goToNextStepPipeline(state));
|
|
963
|
-
```
|
|
964
|
-
|
|
965
|
-
### Pattern 6: Real-time Data Streams
|
|
966
|
-
|
|
967
|
-
**When**: WebSocket updates, SSE, real-time data
|
|
968
|
-
**Where to use**: WebSocket handlers, event listeners
|
|
969
|
-
|
|
970
|
-
```typescript
|
|
971
|
-
import { pipe, filter, map, take } from 'fp-pack';
|
|
972
|
-
|
|
973
|
-
// GOOD: Process incoming WebSocket messages
|
|
974
|
-
const handleWebSocketMessage = pipe(
|
|
975
|
-
(event: MessageEvent) => JSON.parse(event.data),
|
|
976
|
-
filter((msg: Message) => msg.type === 'USER_UPDATE'),
|
|
977
|
-
map(msg => msg.payload),
|
|
978
|
-
(update) => {
|
|
979
|
-
// Update state with new data
|
|
980
|
-
setUsers(prevUsers =>
|
|
981
|
-
prevUsers.map(u => u.id === update.id ? { ...u, ...update } : u)
|
|
982
|
-
);
|
|
983
|
-
}
|
|
984
|
-
);
|
|
985
|
-
|
|
986
|
-
// websocket.onmessage = handleWebSocketMessage;
|
|
987
|
-
|
|
988
|
-
// GOOD: Batch updates with stream
|
|
989
|
-
import { pipe as streamPipe, filter as streamFilter, take as streamTake, toArray } from 'fp-pack/stream';
|
|
990
|
-
import { pipeAsync, runPipeResult } from 'fp-pack';
|
|
991
|
-
|
|
992
|
-
const processBatchUpdates = async (updates: AsyncIterable<Update>) => {
|
|
993
|
-
const processed = await streamPipe(
|
|
994
|
-
streamFilter((u: Update) => u.priority === 'high'),
|
|
995
|
-
streamTake(100),
|
|
996
|
-
toArray
|
|
997
|
-
)(updates);
|
|
998
|
-
|
|
999
|
-
batchUpdateUI(processed);
|
|
1000
|
-
};
|
|
1001
|
-
```
|
|
1002
|
-
|
|
1003
|
-
### Pattern 7: Component Props Transformation
|
|
1004
|
-
|
|
1005
|
-
**When**: Passing data to child components
|
|
1006
|
-
**Where to use**: Component composition, prop drilling
|
|
1007
|
-
|
|
1008
|
-
```typescript
|
|
1009
|
-
import { pipe, pick, map, merge } from 'fp-pack';
|
|
1010
|
-
|
|
1011
|
-
// GOOD: Transform data for child component
|
|
1012
|
-
const prepareUserCardProps = pipe(
|
|
1013
|
-
pick(['id', 'name', 'avatar', 'status']),
|
|
1014
|
-
merge({
|
|
1015
|
-
onClick: handleUserClick,
|
|
1016
|
-
className: 'user-card'
|
|
1017
|
-
})
|
|
1018
|
-
);
|
|
1019
|
-
|
|
1020
|
-
// Usage:
|
|
1021
|
-
// const userProps = prepareUserCardProps(user);
|
|
1022
|
-
// <UserCard {...userProps} />
|
|
1023
|
-
|
|
1024
|
-
// GOOD: Prepare list of component props
|
|
1025
|
-
const prepareListItems = pipe(
|
|
1026
|
-
filter((item: Item) => item.visible),
|
|
1027
|
-
map(item => ({
|
|
1028
|
-
key: item.id,
|
|
1029
|
-
...pick(['title', 'description', 'icon'], item),
|
|
1030
|
-
onClick: () => handleClick(item.id),
|
|
1031
|
-
isActive: item.id === activeId
|
|
1032
|
-
}))
|
|
1033
|
-
);
|
|
1034
|
-
|
|
1035
|
-
// Usage:
|
|
1036
|
-
// {prepareListItems(items).map(props => <ListItem {...props} />)}
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
### Pattern 8: State Update Reducers
|
|
1040
|
-
|
|
1041
|
-
**When**: Complex state updates, global state management
|
|
1042
|
-
**Where to use**: Redux/Zustand/Pinia reducers, state update functions
|
|
1043
|
-
|
|
1044
|
-
```typescript
|
|
1045
|
-
import { pipe, assoc, dissoc, merge, evolve } from 'fp-pack';
|
|
1046
|
-
|
|
1047
|
-
// GOOD: Redux-style reducer with fp-pack
|
|
1048
|
-
const userReducer = (state: State, action: Action) => {
|
|
1049
|
-
switch (action.type) {
|
|
1050
|
-
case 'ADD_USER':
|
|
1051
|
-
return pipe(
|
|
1052
|
-
prop('users'),
|
|
1053
|
-
append(action.payload),
|
|
1054
|
-
(users) => assoc('users', users, state)
|
|
1055
|
-
)(state);
|
|
1056
|
-
|
|
1057
|
-
case 'UPDATE_USER':
|
|
1058
|
-
return pipe(
|
|
1059
|
-
prop('users'),
|
|
1060
|
-
map((u: User) => u.id === action.payload.id
|
|
1061
|
-
? merge(u, action.payload.updates)
|
|
1062
|
-
: u
|
|
1063
|
-
),
|
|
1064
|
-
(users) => assoc('users', users, state)
|
|
1065
|
-
)(state);
|
|
1066
|
-
|
|
1067
|
-
case 'DELETE_USER':
|
|
1068
|
-
return pipe(
|
|
1069
|
-
prop('users'),
|
|
1070
|
-
filter((u: User) => u.id !== action.payload),
|
|
1071
|
-
(users) => assoc('users', users, state)
|
|
1072
|
-
)(state);
|
|
1073
|
-
|
|
1074
|
-
default:
|
|
1075
|
-
return state;
|
|
1076
|
-
}
|
|
1077
|
-
};
|
|
1078
|
-
|
|
1079
|
-
// GOOD: Using evolve for nested updates
|
|
1080
|
-
const updateNestedState = evolve({
|
|
1081
|
-
user: evolve({
|
|
1082
|
-
profile: merge({ verified: true }),
|
|
1083
|
-
settings: assoc('notifications', false)
|
|
1084
|
-
}),
|
|
1085
|
-
lastUpdated: () => new Date()
|
|
1086
|
-
});
|
|
1087
|
-
```
|
|
1088
|
-
|
|
1089
|
-
### Pattern 9: Optimistic Updates
|
|
1090
|
-
|
|
1091
|
-
**When**: UI updates before server confirmation
|
|
1092
|
-
**Where to use**: Create/update/delete operations
|
|
1093
|
-
|
|
1094
|
-
```typescript
|
|
1095
|
-
import { pipe, pipeAsync, append, filter } from 'fp-pack';
|
|
1096
|
-
|
|
1097
|
-
// GOOD: Optimistic create with rollback
|
|
1098
|
-
const createItemOptimistically = (newItem: Item) => {
|
|
1099
|
-
const tempId = `temp-${Date.now()}`;
|
|
1100
|
-
const optimisticItem = { ...newItem, id: tempId, pending: true };
|
|
1101
|
-
|
|
1102
|
-
// Immediately update UI
|
|
1103
|
-
setItems(pipe(append(optimisticItem)));
|
|
1104
|
-
|
|
1105
|
-
// Then persist
|
|
1106
|
-
return pipeAsync(
|
|
1107
|
-
async () => api.createItem(newItem),
|
|
1108
|
-
(savedItem) => {
|
|
1109
|
-
// Replace temp with real item
|
|
1110
|
-
setItems(
|
|
1111
|
-
pipe(
|
|
1112
|
-
filter((item: Item) => item.id !== tempId),
|
|
1113
|
-
append(savedItem)
|
|
1114
|
-
)
|
|
1115
|
-
);
|
|
1116
|
-
return savedItem;
|
|
1117
|
-
}
|
|
1118
|
-
)().catch((error) => {
|
|
1119
|
-
// Rollback on error
|
|
1120
|
-
setItems(pipe(filter((item: Item) => item.id !== tempId)));
|
|
1121
|
-
throw error;
|
|
1122
|
-
});
|
|
1123
|
-
};
|
|
1124
|
-
```
|
|
1125
|
-
|
|
1126
|
-
### Pattern 10: URL/Query Parameter Handling
|
|
1127
|
-
|
|
1128
|
-
**When**: Syncing UI state with URL
|
|
1129
|
-
**Where to use**: Routing, search parameters, filters
|
|
1130
|
-
|
|
1131
|
-
```typescript
|
|
1132
|
-
import { pipe, pick, mapValues, merge } from 'fp-pack';
|
|
1133
|
-
|
|
1134
|
-
// GOOD: Parse query params to state
|
|
1135
|
-
const parseQueryParams = pipe(
|
|
1136
|
-
(search: string) => new URLSearchParams(search),
|
|
1137
|
-
(params) => ({
|
|
1138
|
-
page: Number(params.get('page')) || 1,
|
|
1139
|
-
query: params.get('q') || '',
|
|
1140
|
-
category: params.get('category') || 'all',
|
|
1141
|
-
sort: params.get('sort') || 'date'
|
|
1142
|
-
})
|
|
1143
|
-
);
|
|
1144
|
-
|
|
1145
|
-
// GOOD: Convert state to query params
|
|
1146
|
-
const stateToQueryParams = pipe(
|
|
1147
|
-
pick(['page', 'query', 'category', 'sort']),
|
|
1148
|
-
(state) => {
|
|
1149
|
-
const params = new URLSearchParams();
|
|
1150
|
-
Object.entries(state).forEach(([key, value]) => {
|
|
1151
|
-
if (value) params.set(key, String(value));
|
|
1152
|
-
});
|
|
1153
|
-
return params.toString();
|
|
1154
|
-
}
|
|
1155
|
-
);
|
|
1156
|
-
|
|
1157
|
-
// Usage in framework router:
|
|
1158
|
-
// const filters = parseQueryParams(location.search);
|
|
1159
|
-
// navigate(`/products?${stateToQueryParams(currentState)}`);
|
|
1160
|
-
```
|
|
1161
|
-
|
|
1162
|
-
### Pattern 11: Infinite Scroll / Virtual Lists
|
|
1163
|
-
|
|
1164
|
-
**When**: Large lists with lazy loading, infinite scroll, virtual rendering
|
|
1165
|
-
**Where to use**: Scroll handlers, pagination, large dataset rendering
|
|
1166
|
-
|
|
1167
|
-
```typescript
|
|
1168
|
-
import { pipe, pipeAsyncSideEffect, when, tap, runPipeResult } from 'fp-pack';
|
|
1169
|
-
import { pipe as streamPipe, filter as streamFilter, take as streamTake, toArray } from 'fp-pack/stream';
|
|
1170
|
-
|
|
1171
|
-
// GOOD: Infinite scroll with pipe - all logic inside
|
|
1172
|
-
const handleScroll = pipe(
|
|
1173
|
-
(e: Event) => e.target as HTMLElement,
|
|
1174
|
-
(el) => ({
|
|
1175
|
-
scrollTop: el.scrollTop,
|
|
1176
|
-
scrollHeight: el.scrollHeight,
|
|
1177
|
-
clientHeight: el.clientHeight,
|
|
1178
|
-
hasMore,
|
|
1179
|
-
isLoading
|
|
1180
|
-
}),
|
|
1181
|
-
// Only load if near bottom, has more data, and not currently loading
|
|
1182
|
-
when(
|
|
1183
|
-
({ scrollTop, scrollHeight, clientHeight, hasMore, isLoading }) =>
|
|
1184
|
-
scrollTop + clientHeight >= scrollHeight - 100 && hasMore && !isLoading,
|
|
1185
|
-
tap(() => loadNextPage())
|
|
1186
|
-
)
|
|
1187
|
-
);
|
|
1188
|
-
|
|
1189
|
-
// GOOD: Load next page with stream processing
|
|
1190
|
-
const loadNextPagePipeline = pipeAsyncSideEffect(
|
|
1191
|
-
async () => {
|
|
1192
|
-
setIsLoading(true);
|
|
1193
|
-
return fetchItemsFromAPI(currentPage);
|
|
1194
|
-
},
|
|
1195
|
-
// Use stream for lazy processing
|
|
1196
|
-
(items) => streamPipe(
|
|
1197
|
-
streamFilter((item: Item) => item.visible),
|
|
1198
|
-
streamTake(pageSize),
|
|
1199
|
-
toArray
|
|
1200
|
-
)(items),
|
|
1201
|
-
tap((newItems) => setItems(prev => [...prev, ...newItems])),
|
|
1202
|
-
tap(() => setCurrentPage(prev => prev + 1)),
|
|
1203
|
-
tap(() => setIsLoading(false))
|
|
1204
|
-
);
|
|
1205
|
-
|
|
1206
|
-
const loadNextPage = () => runPipeResult(loadNextPagePipeline());
|
|
1207
|
-
|
|
1208
|
-
// GOOD: Virtual scroll - calculate visible range in pipe
|
|
1209
|
-
const getVisibleItems = pipe(
|
|
1210
|
-
(scrollTop: number) => ({
|
|
1211
|
-
itemHeight: 50,
|
|
1212
|
-
viewportHeight: 600,
|
|
1213
|
-
bufferSize: 5,
|
|
1214
|
-
scrollTop
|
|
1215
|
-
}),
|
|
1216
|
-
({ itemHeight, viewportHeight, bufferSize, scrollTop }) => ({
|
|
1217
|
-
startIndex: Math.floor(scrollTop / itemHeight),
|
|
1218
|
-
endIndex: Math.ceil((scrollTop + viewportHeight) / itemHeight),
|
|
1219
|
-
bufferSize
|
|
1220
|
-
}),
|
|
1221
|
-
({ startIndex, endIndex, bufferSize }) => ({
|
|
1222
|
-
start: Math.max(0, startIndex - bufferSize),
|
|
1223
|
-
end: endIndex + bufferSize
|
|
1224
|
-
}),
|
|
1225
|
-
({ start, end }) => allItems.slice(start, end)
|
|
1226
|
-
);
|
|
1227
|
-
|
|
1228
|
-
// GOOD: Lazy load with intersection observer
|
|
1229
|
-
const handleIntersection = pipe(
|
|
1230
|
-
(entries: IntersectionObserverEntry[]) => entries[0],
|
|
1231
|
-
when(
|
|
1232
|
-
(entry) => entry.isIntersecting && hasMoreItems && !isLoadingMore,
|
|
1233
|
-
tap(() => loadMoreItems())
|
|
1234
|
-
)
|
|
1235
|
-
);
|
|
1236
|
-
```
|
|
1237
|
-
|
|
1238
|
-
### Pattern 12: Conditional State Updates
|
|
1239
|
-
|
|
1240
|
-
**When**: State updates that depend on conditions
|
|
1241
|
-
**Where to use**: Any state update with business logic
|
|
1242
|
-
|
|
1243
|
-
```typescript
|
|
1244
|
-
import { pipe, ifElse, when, cond, tap, assoc, prop, append, map, filter, merge } from 'fp-pack';
|
|
1245
|
-
|
|
1246
|
-
// GOOD: Use ifElse instead of if/else
|
|
1247
|
-
const toggleUserStatus = pipe(
|
|
1248
|
-
ifElse(
|
|
1249
|
-
(user: User) => user.status === 'active',
|
|
1250
|
-
assoc('status', 'inactive'),
|
|
1251
|
-
assoc('status', 'active')
|
|
1252
|
-
),
|
|
1253
|
-
tap((updatedUser) => setUser(updatedUser))
|
|
1254
|
-
);
|
|
1255
|
-
|
|
1256
|
-
// GOOD: Use ifElse for conditional side effects
|
|
1257
|
-
const saveIfValid = pipe(
|
|
1258
|
-
validateForm,
|
|
1259
|
-
ifElse(
|
|
1260
|
-
(result) => result.isValid,
|
|
1261
|
-
pipe(
|
|
1262
|
-
tap((data) => saveToAPI(data)),
|
|
1263
|
-
tap(() => showSuccessMessage())
|
|
1264
|
-
),
|
|
1265
|
-
tap((result) => setErrors(result.errors))
|
|
1266
|
-
)
|
|
1267
|
-
);
|
|
1268
|
-
|
|
1269
|
-
// GOOD: Use cond for multi-branch logic (instead of switch/if-else chain)
|
|
1270
|
-
const processUserAction = pipe(
|
|
1271
|
-
prop('action'),
|
|
1272
|
-
cond([
|
|
1273
|
-
[
|
|
1274
|
-
(action) => action.type === 'CREATE',
|
|
1275
|
-
pipe(
|
|
1276
|
-
prop('payload'),
|
|
1277
|
-
(user) => append(user),
|
|
1278
|
-
tap((users) => setUsers(users))
|
|
1279
|
-
)
|
|
1280
|
-
],
|
|
1281
|
-
[
|
|
1282
|
-
(action) => action.type === 'UPDATE',
|
|
1283
|
-
pipe(
|
|
1284
|
-
prop('payload'),
|
|
1285
|
-
({ id, updates }) => map((u: User) => u.id === id ? merge(u, updates) : u),
|
|
1286
|
-
tap((users) => setUsers(users))
|
|
1287
|
-
)
|
|
1288
|
-
],
|
|
1289
|
-
[
|
|
1290
|
-
(action) => action.type === 'DELETE',
|
|
1291
|
-
pipe(
|
|
1292
|
-
prop('payload'),
|
|
1293
|
-
(id) => filter((u: User) => u.id !== id),
|
|
1294
|
-
tap((users) => setUsers(users))
|
|
1295
|
-
)
|
|
1296
|
-
],
|
|
1297
|
-
[
|
|
1298
|
-
() => true, // default case
|
|
1299
|
-
tap(() => console.warn('Unknown action'))
|
|
1300
|
-
]
|
|
1301
|
-
])
|
|
1302
|
-
);
|
|
1303
|
-
|
|
1304
|
-
// GOOD: Complex state update with all logic in pipe
|
|
1305
|
-
const updateCartItem = (itemId: string, quantity: number) => pipe(
|
|
1306
|
-
// Get current cart
|
|
1307
|
-
(cart) => cart.items,
|
|
1308
|
-
// Find and update item
|
|
1309
|
-
map((item: CartItem) =>
|
|
1310
|
-
ifElse(
|
|
1311
|
-
() => item.id === itemId,
|
|
1312
|
-
pipe(
|
|
1313
|
-
assoc('quantity', quantity),
|
|
1314
|
-
when(
|
|
1315
|
-
(updated) => updated.quantity <= 0,
|
|
1316
|
-
() => null // Mark for removal
|
|
1317
|
-
)
|
|
1318
|
-
),
|
|
1319
|
-
() => item
|
|
1320
|
-
)(item)
|
|
1321
|
-
),
|
|
1322
|
-
// Remove null items (quantity <= 0)
|
|
1323
|
-
filter((item) => item !== null),
|
|
1324
|
-
// Update state
|
|
1325
|
-
tap((items) => setCart({ items })),
|
|
1326
|
-
// Show notification
|
|
1327
|
-
tap((items) => {
|
|
1328
|
-
const item = items.find(i => i.id === itemId);
|
|
1329
|
-
if (item) showNotification(`Updated ${item.name}`);
|
|
1330
|
-
else showNotification('Item removed from cart');
|
|
1331
|
-
})
|
|
1332
|
-
);
|
|
1333
|
-
```
|
|
1334
|
-
|
|
1335
|
-
## Library Integration Quick Reference
|
|
1336
|
-
|
|
1337
|
-
This section shows how to integrate fp-pack with popular UI libraries. All examples keep logic **inside pipe chains** using fp-pack control flow functions.
|
|
1338
|
-
|
|
1339
|
-
### React Ecosystem
|
|
1340
|
-
|
|
1341
|
-
#### React Query / TanStack Query
|
|
1342
|
-
|
|
1343
|
-
```typescript
|
|
1344
|
-
import { useQuery, useMutation } from '@tanstack/react-query';
|
|
1345
|
-
import { pipe, filter, sortBy, map, tap, when } from 'fp-pack';
|
|
1346
|
-
|
|
1347
|
-
// GOOD: Transform data in select using pipe
|
|
1348
|
-
const { data: activeUsers } = useQuery({
|
|
1349
|
-
queryKey: ['users'],
|
|
1350
|
-
queryFn: fetchUsers,
|
|
1351
|
-
select: pipe(
|
|
1352
|
-
filter((u: User) => u.status === 'active'),
|
|
1353
|
-
sortBy((u) => u.name),
|
|
1354
|
-
map((u) => ({ id: u.id, name: u.name, email: u.email }))
|
|
1355
|
-
)
|
|
1356
|
-
});
|
|
1357
|
-
|
|
1358
|
-
// GOOD: Handle mutations with pipe
|
|
1359
|
-
const mutation = useMutation({
|
|
1360
|
-
mutationFn: createUser,
|
|
1361
|
-
onSuccess: pipe(
|
|
1362
|
-
tap((newUser) => queryClient.invalidateQueries(['users'])),
|
|
1363
|
-
when(
|
|
1364
|
-
(user) => user.isPremium,
|
|
1365
|
-
tap(() => showPremiumWelcome())
|
|
1366
|
-
),
|
|
1367
|
-
tap(() => navigate('/dashboard'))
|
|
1368
|
-
)
|
|
1369
|
-
});
|
|
1370
|
-
|
|
1371
|
-
// GOOD: Optimistic updates in pipe
|
|
1372
|
-
const updateMutation = useMutation({
|
|
1373
|
-
mutationFn: updateUser,
|
|
1374
|
-
onMutate: pipe(
|
|
1375
|
-
tap(async (newUser) => {
|
|
1376
|
-
await queryClient.cancelQueries(['users']);
|
|
1377
|
-
const previous = queryClient.getQueryData(['users']);
|
|
1378
|
-
queryClient.setQueryData(['users'], pipe(
|
|
1379
|
-
map((u: User) => u.id === newUser.id ? merge(u, newUser) : u)
|
|
1380
|
-
));
|
|
1381
|
-
return { previous };
|
|
1382
|
-
})
|
|
1383
|
-
),
|
|
1384
|
-
onError: pipe(
|
|
1385
|
-
tap((err, variables, context) => {
|
|
1386
|
-
if (context?.previous) {
|
|
1387
|
-
queryClient.setQueryData(['users'], context.previous);
|
|
1388
|
-
}
|
|
1389
|
-
})
|
|
1390
|
-
)
|
|
1391
|
-
});
|
|
1392
|
-
```
|
|
1393
|
-
|
|
1394
|
-
#### Zustand
|
|
1395
|
-
|
|
1396
|
-
```typescript
|
|
1397
|
-
import create from 'zustand';
|
|
1398
|
-
import { pipe, append, filter, map, merge, ifElse, when, tap, prop, sortBy, assoc } from 'fp-pack';
|
|
1399
|
-
|
|
1400
|
-
// GOOD: All actions use pipe
|
|
1401
|
-
const useStore = create((set, get) => ({
|
|
1402
|
-
users: [],
|
|
1403
|
-
|
|
1404
|
-
addUser: pipe(
|
|
1405
|
-
(user: User) => user,
|
|
1406
|
-
when(
|
|
1407
|
-
(user) => !get().users.some(u => u.id === user.id),
|
|
1408
|
-
tap((user) => set(pipe(
|
|
1409
|
-
prop('users'),
|
|
1410
|
-
append(user),
|
|
1411
|
-
sortBy((u: User) => u.name),
|
|
1412
|
-
(users) => ({ users })
|
|
1413
|
-
)(get())))
|
|
1414
|
-
)
|
|
1415
|
-
),
|
|
1416
|
-
|
|
1417
|
-
updateUser: (id: string, updates: Partial<User>) => set(pipe(
|
|
1418
|
-
prop('users'),
|
|
1419
|
-
map((u: User) =>
|
|
1420
|
-
ifElse(
|
|
1421
|
-
() => u.id === id,
|
|
1422
|
-
merge(u, updates),
|
|
1423
|
-
() => u
|
|
1424
|
-
)(u)
|
|
1425
|
-
),
|
|
1426
|
-
(users) => ({ users })
|
|
1427
|
-
)(get())),
|
|
1428
|
-
|
|
1429
|
-
deleteUser: (id: string) => set(pipe(
|
|
1430
|
-
prop('users'),
|
|
1431
|
-
filter((u: User) => u.id !== id),
|
|
1432
|
-
(users) => ({ users })
|
|
1433
|
-
)(get())),
|
|
1434
|
-
|
|
1435
|
-
toggleUserStatus: (id: string) => set(pipe(
|
|
1436
|
-
prop('users'),
|
|
1437
|
-
map((u: User) => u.id === id
|
|
1438
|
-
? pipe(
|
|
1439
|
-
ifElse(
|
|
1440
|
-
(user) => user.status === 'active',
|
|
1441
|
-
assoc('status', 'inactive'),
|
|
1442
|
-
assoc('status', 'active')
|
|
1443
|
-
)
|
|
1444
|
-
)(u)
|
|
1445
|
-
: u
|
|
1446
|
-
),
|
|
1447
|
-
(users) => ({ users })
|
|
1448
|
-
)(get()))
|
|
1449
|
-
}));
|
|
1450
|
-
```
|
|
1451
|
-
|
|
1452
|
-
#### Redux Toolkit
|
|
1453
|
-
|
|
1454
|
-
```typescript
|
|
1455
|
-
import { createSlice } from '@reduxjs/toolkit';
|
|
1456
|
-
import { pipe, append, filter, map, merge, sortBy, cond, assoc } from 'fp-pack';
|
|
1457
|
-
|
|
1458
|
-
// GOOD: Reducers with pipe - no manual mutations
|
|
1459
|
-
const userSlice = createSlice({
|
|
1460
|
-
name: 'users',
|
|
1461
|
-
initialState: { list: [], loading: false },
|
|
1462
|
-
reducers: {
|
|
1463
|
-
addUser: (state, action) => {
|
|
1464
|
-
state.list = pipe(
|
|
1465
|
-
append(action.payload),
|
|
1466
|
-
sortBy((u: User) => u.createdAt)
|
|
1467
|
-
)(state.list);
|
|
1468
|
-
},
|
|
1469
|
-
|
|
1470
|
-
updateUser: (state, action) => {
|
|
1471
|
-
state.list = pipe(
|
|
1472
|
-
map((u: User) =>
|
|
1473
|
-
u.id === action.payload.id
|
|
1474
|
-
? merge(u, action.payload.updates)
|
|
1475
|
-
: u
|
|
1476
|
-
)
|
|
1477
|
-
)(state.list);
|
|
1478
|
-
},
|
|
1479
|
-
|
|
1480
|
-
deleteUser: (state, action) => {
|
|
1481
|
-
state.list = pipe(
|
|
1482
|
-
filter((u: User) => u.id !== action.payload)
|
|
1483
|
-
)(state.list);
|
|
1484
|
-
},
|
|
1485
|
-
|
|
1486
|
-
// Complex update with cond
|
|
1487
|
-
processAction: (state, action) => {
|
|
1488
|
-
state.list = pipe(
|
|
1489
|
-
cond([
|
|
1490
|
-
[
|
|
1491
|
-
() => action.type === 'BULK_ACTIVATE',
|
|
1492
|
-
map((u: User) => assoc('status', 'active', u))
|
|
1493
|
-
],
|
|
1494
|
-
[
|
|
1495
|
-
() => action.type === 'BULK_DELETE',
|
|
1496
|
-
filter((u: User) => !action.payload.ids.includes(u.id))
|
|
1497
|
-
],
|
|
1498
|
-
[
|
|
1499
|
-
() => true,
|
|
1500
|
-
(users) => users // no change
|
|
1501
|
-
]
|
|
1502
|
-
])
|
|
1503
|
-
)(state.list);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
});
|
|
1507
|
-
```
|
|
1508
|
-
|
|
1509
|
-
#### React Hook Form
|
|
1510
|
-
|
|
1511
|
-
```typescript
|
|
1512
|
-
import { useForm } from 'react-hook-form';
|
|
1513
|
-
import { pipe, pipeSideEffect, pipeAsyncSideEffect, pick, mapValues, trim, when, tap, SideEffect, runPipeResult } from 'fp-pack';
|
|
1514
|
-
|
|
1515
|
-
// GOOD: Validation with pipeSideEffect
|
|
1516
|
-
const validateFormDataPipeline = pipeSideEffect(
|
|
1517
|
-
pick(['email', 'password', 'name']),
|
|
1518
|
-
mapValues((v) => typeof v === 'string' ? trim(v) : v),
|
|
1519
|
-
(data) => {
|
|
1520
|
-
const errors: any = {};
|
|
1521
|
-
if (!data.email?.includes('@')) errors.email = 'Invalid email';
|
|
1522
|
-
if ((data.password?.length || 0) < 8) errors.password = 'Too short';
|
|
1523
|
-
return Object.keys(errors).length > 0
|
|
1524
|
-
? SideEffect.of(() => ({ values: {}, errors }), 'VALIDATION_ERROR')
|
|
1525
|
-
: { values: data, errors: {} };
|
|
1526
|
-
}
|
|
1527
|
-
);
|
|
1528
|
-
|
|
1529
|
-
const validateFormData = (values: any) => runPipeResult(validateFormDataPipeline(values));
|
|
1530
|
-
|
|
1531
|
-
const { register, handleSubmit } = useForm({
|
|
1532
|
-
resolver: (values) => validateFormData(values)
|
|
1533
|
-
});
|
|
1534
|
-
|
|
1535
|
-
// GOOD: Submit handler with pipeAsyncSideEffect
|
|
1536
|
-
const onSubmitPipeline = pipeAsyncSideEffect(
|
|
1537
|
-
validateFormData,
|
|
1538
|
-
when(
|
|
1539
|
-
(result) => Object.keys(result.errors).length === 0,
|
|
1540
|
-
pipe(
|
|
1541
|
-
prop('values'),
|
|
1542
|
-
submitToAPI,
|
|
1543
|
-
tap(() => navigate('/success'))
|
|
1544
|
-
)
|
|
1545
|
-
)
|
|
1546
|
-
);
|
|
1547
|
-
|
|
1548
|
-
const onSubmit = (data: any) => runPipeResult(onSubmitPipeline(data));
|
|
1549
|
-
```
|
|
404
|
+
---
|
|
1550
405
|
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
#### Pinia
|
|
1554
|
-
|
|
1555
|
-
```typescript
|
|
1556
|
-
import { defineStore } from 'pinia';
|
|
1557
|
-
import { pipe, append, filter, map, merge, sortBy, when, tap } from 'fp-pack';
|
|
1558
|
-
|
|
1559
|
-
// GOOD: All actions use pipe
|
|
1560
|
-
export const useUserStore = defineStore('user', {
|
|
1561
|
-
state: () => ({ users: [], loading: false }),
|
|
1562
|
-
|
|
1563
|
-
actions: {
|
|
1564
|
-
addUser(user: User) {
|
|
1565
|
-
this.users = pipe(
|
|
1566
|
-
append(user),
|
|
1567
|
-
sortBy((u: User) => u.name),
|
|
1568
|
-
when(
|
|
1569
|
-
(users) => users.length > 100,
|
|
1570
|
-
tap(() => this.showWarning('Many users'))
|
|
1571
|
-
)
|
|
1572
|
-
)(this.users);
|
|
1573
|
-
},
|
|
1574
|
-
|
|
1575
|
-
updateUser(id: string, updates: Partial<User>) {
|
|
1576
|
-
this.users = pipe(
|
|
1577
|
-
map((u: User) => u.id === id ? merge(u, updates) : u)
|
|
1578
|
-
)(this.users);
|
|
1579
|
-
},
|
|
1580
|
-
|
|
1581
|
-
deleteUser(id: string) {
|
|
1582
|
-
this.users = pipe(
|
|
1583
|
-
filter((u: User) => u.id !== id),
|
|
1584
|
-
tap((users) => {
|
|
1585
|
-
if (users.length === 0) this.showEmptyState = true;
|
|
1586
|
-
})
|
|
1587
|
-
)(this.users);
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
});
|
|
1591
|
-
```
|
|
406
|
+
## Anti-Patterns (Avoid)
|
|
1592
407
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
// GOOD: Transform response with pipe
|
|
1600
|
-
const { data } = useFetch('/api/users', {
|
|
1601
|
-
afterFetch: pipe(
|
|
1602
|
-
prop('data'),
|
|
1603
|
-
filter((u: User) => u.verified),
|
|
1604
|
-
sortBy((u) => u.name),
|
|
1605
|
-
map((u) => ({ id: u.id, name: u.name })),
|
|
1606
|
-
tap((users) => console.log(`Loaded ${users.length} users`))
|
|
1607
|
-
)
|
|
1608
|
-
}).json();
|
|
1609
|
-
|
|
1610
|
-
// GOOD: Refetch with condition in pipe
|
|
1611
|
-
const { execute } = useFetch('/api/users');
|
|
1612
|
-
|
|
1613
|
-
const refreshIfNeeded = pipe(
|
|
1614
|
-
(lastUpdate: Date) => Date.now() - lastUpdate.getTime(),
|
|
1615
|
-
when(
|
|
1616
|
-
(diff) => diff > 5 * 60 * 1000, // 5 minutes
|
|
1617
|
-
tap(() => execute())
|
|
1618
|
-
)
|
|
1619
|
-
);
|
|
1620
|
-
```
|
|
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
|
|
1621
413
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
All state management libraries benefit from fp-pack's immutable update patterns:
|
|
1625
|
-
|
|
1626
|
-
```typescript
|
|
1627
|
-
// GOOD: Generic state update pattern (works with any library)
|
|
1628
|
-
const updateState = <T>(
|
|
1629
|
-
state: T,
|
|
1630
|
-
path: string[],
|
|
1631
|
-
updater: (value: any) => any
|
|
1632
|
-
) => pipe(
|
|
1633
|
-
pathOr(null, path),
|
|
1634
|
-
updater,
|
|
1635
|
-
(newValue) => assocPath(path, newValue, state)
|
|
1636
|
-
)(state);
|
|
1637
|
-
|
|
1638
|
-
// Usage in any framework:
|
|
1639
|
-
// Redux: return updateState(state, ['users', 0, 'name'], toUpper);
|
|
1640
|
-
// Zustand: set(updateState(get(), ['users', 0, 'name'], toUpper));
|
|
1641
|
-
// Pinia: this.$state = updateState(this.$state, ['users', 0, 'name'], toUpper);
|
|
1642
|
-
```
|
|
414
|
+
---
|
|
1643
415
|
|
|
1644
|
-
##
|
|
416
|
+
## Quick Signature Lookup (When Unsure)
|
|
1645
417
|
|
|
1646
|
-
|
|
418
|
+
Prefer local types first:
|
|
419
|
+
- Main types: `dist/index.d.ts`
|
|
420
|
+
- Stream types: `dist/stream/index.d.ts`
|
|
1647
421
|
|
|
1648
|
-
|
|
1649
|
-
-
|
|
1650
|
-
-
|
|
1651
|
-
- **Svelte**: `$: result = pipe(...)(data)`
|
|
1652
|
-
- **Solid**: `createMemo(() => pipe(...)(data()))`
|
|
422
|
+
If you’re in a consumer project:
|
|
423
|
+
- `node_modules/fp-pack/dist/index.d.ts`
|
|
424
|
+
- `node_modules/fp-pack/dist/stream/index.d.ts`
|
|
1653
425
|
|
|
1654
|
-
|
|
1655
|
-
- **React**: `<button onClick={pipe(...)}>` or `const handler = pipe(...)`
|
|
1656
|
-
- **Vue**: `<button @click="pipe(...)">` or `const handler = pipe(...)`
|
|
1657
|
-
- **Svelte**: `<button on:click={pipe(...)}>` or `const handler = pipe(...)`
|
|
426
|
+
---
|
|
1658
427
|
|
|
1659
|
-
|
|
1660
|
-
- **React**: `useEffect(() => { pipeAsync(...)() }, [deps])`
|
|
1661
|
-
- **Vue**: `watchEffect(() => pipeAsync(...)())`
|
|
1662
|
-
- **Svelte**: `onMount(() => pipeAsync(...)())`
|
|
428
|
+
## Writing New Helpers (If Needed)
|
|
1663
429
|
|
|
1664
|
-
|
|
1665
|
-
- **
|
|
1666
|
-
- **
|
|
1667
|
-
-
|
|
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.
|
|
1668
434
|
|
|
1669
|
-
|
|
435
|
+
---
|
|
1670
436
|
|
|
1671
437
|
## Summary
|
|
1672
438
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
1. **Default to `pipe`** for all data transformations
|
|
1676
|
-
2. **Switch to `pipeAsync`** when async operations are involved (use `pipeAsyncSideEffect` if SideEffect is in the flow)
|
|
1677
|
-
3. **Use `stream/*`** for lazy, memory-efficient processing
|
|
1678
|
-
4. **Handle errors with `SideEffect`** in `pipeSideEffect`/`pipeAsyncSideEffect`, not try-catch
|
|
1679
|
-
5. **Avoid imperative loops** - use fp-pack's declarative functions
|
|
1680
|
-
6. **Never suggest monads** - use SideEffect pattern instead
|
|
1681
|
-
7. **Keep code declarative** - describe what, not how
|
|
1682
|
-
8. **All logic inside pipe** - never break out of pipe chains for conditionals or loops
|
|
1683
|
-
9. **Use control flow functions** - `when`, `unless`, `ifElse`, `cond` instead of if/else/switch
|
|
1684
|
-
10. **Call `runPipeResult` OUTSIDE pipelines** - `runPipeResult` / `matchSideEffect` must be called outside `pipeSideEffect`/`pipeAsyncSideEffect` for proper type safety
|
|
1685
|
-
11. **Use `isSideEffect` for type narrowing** - get precise types in both success and error branches
|
|
1686
|
-
12. **Apply use-case patterns** - recognize scenarios (form handling, list processing, etc.) and apply appropriate fp-pack patterns
|
|
1687
|
-
13. **Framework-agnostic core** - write fp-pack logic independent of UI framework, only wrap at the boundaries
|
|
1688
|
-
14. **Library integration** - use pipe in select/resolver/action functions of popular libraries (React Query, Zustand, Pinia, etc.)
|
|
1689
|
-
|
|
1690
|
-
### Key Principles
|
|
1691
|
-
|
|
1692
|
-
**✅ DO: Keep everything in pipe**
|
|
1693
|
-
```typescript
|
|
1694
|
-
// GOOD: All logic inside pipe
|
|
1695
|
-
const handleSubmitPipeline = pipeAsyncSideEffect(
|
|
1696
|
-
getFormData,
|
|
1697
|
-
validateFields,
|
|
1698
|
-
when(isValid, submitToAPI),
|
|
1699
|
-
unless(isValid, showErrors)
|
|
1700
|
-
);
|
|
1701
|
-
|
|
1702
|
-
const handleSubmit = (form: HTMLFormElement) => runPipeResult(handleSubmitPipeline(form));
|
|
1703
|
-
```
|
|
1704
|
-
|
|
1705
|
-
**❌ DON'T: Break out of pipe for conditionals**
|
|
1706
|
-
```typescript
|
|
1707
|
-
// BAD: Breaking pipe for if/else
|
|
1708
|
-
const handleSubmit = pipe(
|
|
1709
|
-
getFormData,
|
|
1710
|
-
validateFields
|
|
1711
|
-
);
|
|
1712
|
-
const result = handleSubmit(form);
|
|
1713
|
-
if (result.isValid) { // ❌ Outside pipe
|
|
1714
|
-
submitToAPI(result);
|
|
1715
|
-
} else {
|
|
1716
|
-
showErrors(result.errors);
|
|
1717
|
-
}
|
|
1718
|
-
```
|
|
1719
|
-
|
|
1720
|
-
**✅ DO: Use when/cond/ifElse for branching**
|
|
1721
|
-
```typescript
|
|
1722
|
-
// GOOD: Branching inside pipe
|
|
1723
|
-
const processAction = pipe(
|
|
1724
|
-
cond([
|
|
1725
|
-
[(action) => action.type === 'CREATE', handleCreate],
|
|
1726
|
-
[(action) => action.type === 'UPDATE', handleUpdate],
|
|
1727
|
-
[(action) => action.type === 'DELETE', handleDelete],
|
|
1728
|
-
[() => true, handleDefault]
|
|
1729
|
-
])
|
|
1730
|
-
);
|
|
1731
|
-
```
|
|
1732
|
-
|
|
1733
|
-
**❌ DON'T: Use switch/if-else chains**
|
|
1734
|
-
```typescript
|
|
1735
|
-
// BAD: Imperative branching
|
|
1736
|
-
const processAction = (action) => {
|
|
1737
|
-
switch (action.type) { // ❌ Imperative
|
|
1738
|
-
case 'CREATE': return handleCreate(action);
|
|
1739
|
-
case 'UPDATE': return handleUpdate(action);
|
|
1740
|
-
case 'DELETE': return handleDelete(action);
|
|
1741
|
-
default: return handleDefault(action);
|
|
1742
|
-
}
|
|
1743
|
-
};
|
|
1744
|
-
```
|
|
1745
|
-
|
|
1746
|
-
Your goal is to write clean, readable, functional code that leverages fp-pack's full potential in real-world UI applications.
|
|
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 (with generics if inference widens to `any`).
|