@dojocho/effect-ts 0.0.1
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/DOJO.md +22 -0
- package/dojo.json +50 -0
- package/katas/001-hello-effect/SENSEI.md +72 -0
- package/katas/001-hello-effect/solution.test.ts +35 -0
- package/katas/001-hello-effect/solution.ts +16 -0
- package/katas/002-transform-with-map/SENSEI.md +72 -0
- package/katas/002-transform-with-map/solution.test.ts +33 -0
- package/katas/002-transform-with-map/solution.ts +16 -0
- package/katas/003-generator-pipelines/SENSEI.md +72 -0
- package/katas/003-generator-pipelines/solution.test.ts +40 -0
- package/katas/003-generator-pipelines/solution.ts +29 -0
- package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
- package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
- package/katas/004-flatmap-and-chaining/solution.ts +18 -0
- package/katas/005-pipe-composition/SENSEI.md +81 -0
- package/katas/005-pipe-composition/solution.test.ts +41 -0
- package/katas/005-pipe-composition/solution.ts +19 -0
- package/katas/006-handle-errors/SENSEI.md +86 -0
- package/katas/006-handle-errors/solution.test.ts +53 -0
- package/katas/006-handle-errors/solution.ts +30 -0
- package/katas/007-tagged-errors/SENSEI.md +79 -0
- package/katas/007-tagged-errors/solution.test.ts +82 -0
- package/katas/007-tagged-errors/solution.ts +37 -0
- package/katas/008-error-patterns/SENSEI.md +89 -0
- package/katas/008-error-patterns/solution.test.ts +41 -0
- package/katas/008-error-patterns/solution.ts +38 -0
- package/katas/009-option-type/SENSEI.md +96 -0
- package/katas/009-option-type/solution.test.ts +49 -0
- package/katas/009-option-type/solution.ts +26 -0
- package/katas/010-either-and-exit/SENSEI.md +86 -0
- package/katas/010-either-and-exit/solution.test.ts +33 -0
- package/katas/010-either-and-exit/solution.ts +17 -0
- package/katas/011-services-and-context/SENSEI.md +82 -0
- package/katas/011-services-and-context/solution.test.ts +23 -0
- package/katas/011-services-and-context/solution.ts +17 -0
- package/katas/012-layers/SENSEI.md +73 -0
- package/katas/012-layers/solution.test.ts +23 -0
- package/katas/012-layers/solution.ts +26 -0
- package/katas/013-testing-effects/SENSEI.md +88 -0
- package/katas/013-testing-effects/solution.test.ts +41 -0
- package/katas/013-testing-effects/solution.ts +20 -0
- package/katas/014-schema-basics/SENSEI.md +81 -0
- package/katas/014-schema-basics/solution.test.ts +35 -0
- package/katas/014-schema-basics/solution.ts +25 -0
- package/katas/015-domain-modeling/SENSEI.md +85 -0
- package/katas/015-domain-modeling/solution.test.ts +46 -0
- package/katas/015-domain-modeling/solution.ts +42 -0
- package/katas/016-retry-and-schedule/SENSEI.md +72 -0
- package/katas/016-retry-and-schedule/solution.test.ts +26 -0
- package/katas/016-retry-and-schedule/solution.ts +23 -0
- package/katas/017-parallel-effects/SENSEI.md +70 -0
- package/katas/017-parallel-effects/solution.test.ts +33 -0
- package/katas/017-parallel-effects/solution.ts +17 -0
- package/katas/018-race-and-timeout/SENSEI.md +75 -0
- package/katas/018-race-and-timeout/solution.test.ts +30 -0
- package/katas/018-race-and-timeout/solution.ts +27 -0
- package/katas/019-ref-and-state/SENSEI.md +72 -0
- package/katas/019-ref-and-state/solution.test.ts +29 -0
- package/katas/019-ref-and-state/solution.ts +16 -0
- package/katas/020-fibers/SENSEI.md +80 -0
- package/katas/020-fibers/solution.test.ts +23 -0
- package/katas/020-fibers/solution.ts +23 -0
- package/katas/021-acquire-release/SENSEI.md +57 -0
- package/katas/021-acquire-release/solution.test.ts +23 -0
- package/katas/021-acquire-release/solution.ts +22 -0
- package/katas/022-scoped-layers/SENSEI.md +52 -0
- package/katas/022-scoped-layers/solution.test.ts +35 -0
- package/katas/022-scoped-layers/solution.ts +19 -0
- package/katas/023-resource-patterns/SENSEI.md +52 -0
- package/katas/023-resource-patterns/solution.test.ts +20 -0
- package/katas/023-resource-patterns/solution.ts +13 -0
- package/katas/024-streams-basics/SENSEI.md +61 -0
- package/katas/024-streams-basics/solution.test.ts +30 -0
- package/katas/024-streams-basics/solution.ts +16 -0
- package/katas/025-stream-operations/SENSEI.md +59 -0
- package/katas/025-stream-operations/solution.test.ts +26 -0
- package/katas/025-stream-operations/solution.ts +17 -0
- package/katas/026-combining-streams/SENSEI.md +54 -0
- package/katas/026-combining-streams/solution.test.ts +20 -0
- package/katas/026-combining-streams/solution.ts +16 -0
- package/katas/027-data-pipelines/SENSEI.md +58 -0
- package/katas/027-data-pipelines/solution.test.ts +22 -0
- package/katas/027-data-pipelines/solution.ts +16 -0
- package/katas/028-logging-and-spans/SENSEI.md +58 -0
- package/katas/028-logging-and-spans/solution.test.ts +50 -0
- package/katas/028-logging-and-spans/solution.ts +20 -0
- package/katas/029-http-client/SENSEI.md +59 -0
- package/katas/029-http-client/solution.test.ts +49 -0
- package/katas/029-http-client/solution.ts +24 -0
- package/katas/030-capstone/SENSEI.md +63 -0
- package/katas/030-capstone/solution.test.ts +67 -0
- package/katas/030-capstone/solution.ts +55 -0
- package/katas/031-config-and-environment/SENSEI.md +77 -0
- package/katas/031-config-and-environment/solution.test.ts +38 -0
- package/katas/031-config-and-environment/solution.ts +11 -0
- package/katas/032-cause-and-defects/SENSEI.md +90 -0
- package/katas/032-cause-and-defects/solution.test.ts +50 -0
- package/katas/032-cause-and-defects/solution.ts +23 -0
- package/katas/033-pattern-matching/SENSEI.md +86 -0
- package/katas/033-pattern-matching/solution.test.ts +36 -0
- package/katas/033-pattern-matching/solution.ts +28 -0
- package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
- package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
- package/katas/034-deferred-and-coordination/solution.ts +24 -0
- package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
- package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
- package/katas/035-queue-and-backpressure/solution.ts +21 -0
- package/katas/036-schema-advanced/SENSEI.md +81 -0
- package/katas/036-schema-advanced/solution.test.ts +55 -0
- package/katas/036-schema-advanced/solution.ts +19 -0
- package/katas/037-cache-and-memoization/SENSEI.md +73 -0
- package/katas/037-cache-and-memoization/solution.test.ts +47 -0
- package/katas/037-cache-and-memoization/solution.ts +24 -0
- package/katas/038-metrics/SENSEI.md +91 -0
- package/katas/038-metrics/solution.test.ts +39 -0
- package/katas/038-metrics/solution.ts +23 -0
- package/katas/039-managed-runtime/SENSEI.md +75 -0
- package/katas/039-managed-runtime/solution.test.ts +29 -0
- package/katas/039-managed-runtime/solution.ts +19 -0
- package/katas/040-request-batching/SENSEI.md +87 -0
- package/katas/040-request-batching/solution.test.ts +56 -0
- package/katas/040-request-batching/solution.ts +32 -0
- package/package.json +22 -0
- package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
- package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
- package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
- package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
- package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
- package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
- package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
- package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
- package/skills/effect-patterns-error-management/SKILL.md +1668 -0
- package/skills/effect-patterns-getting-started/SKILL.md +237 -0
- package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
- package/skills/effect-patterns-observability/SKILL.md +1586 -0
- package/skills/effect-patterns-platform/SKILL.md +1195 -0
- package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
- package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
- package/skills/effect-patterns-resource-management/SKILL.md +827 -0
- package/skills/effect-patterns-scheduling/SKILL.md +451 -0
- package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
- package/skills/effect-patterns-streams/SKILL.md +2052 -0
- package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
- package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
- package/skills/effect-patterns-testing/SKILL.md +1632 -0
- package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
- package/skills/effect-patterns-value-handling/SKILL.md +676 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +3 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-patterns-value-handling
|
|
3
|
+
description: Effect-TS patterns for Value Handling. Use when working with value handling in Effect-TS applications.
|
|
4
|
+
---
|
|
5
|
+
# Effect-TS Patterns: Value Handling
|
|
6
|
+
This skill provides 2 curated Effect-TS patterns for value handling.
|
|
7
|
+
Use this skill when working on tasks related to:
|
|
8
|
+
- value handling
|
|
9
|
+
- Best practices in Effect-TS applications
|
|
10
|
+
- Real-world patterns and solutions
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🟡 Intermediate Patterns
|
|
15
|
+
|
|
16
|
+
### Optional Pattern 1: Handling None and Some Values
|
|
17
|
+
|
|
18
|
+
**Rule:** Use Option to represent values that may not exist, replacing null/undefined with type-safe Option that forces explicit handling.
|
|
19
|
+
|
|
20
|
+
**Good Example:**
|
|
21
|
+
|
|
22
|
+
This example demonstrates Option handling patterns.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { Effect, Option } from "effect";
|
|
26
|
+
|
|
27
|
+
interface User {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
email: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Profile {
|
|
34
|
+
bio: string;
|
|
35
|
+
website?: string;
|
|
36
|
+
location?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const program = Effect.gen(function* () {
|
|
40
|
+
console.log(
|
|
41
|
+
`\n[OPTION HANDLING] None/Some values and pattern matching\n`
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Example 1: Creating Options
|
|
45
|
+
console.log(`[1] Creating Option values:\n`);
|
|
46
|
+
|
|
47
|
+
const someValue: Option.Option<string> = Option.some("data");
|
|
48
|
+
const noneValue: Option.Option<string> = Option.none();
|
|
49
|
+
|
|
50
|
+
const displayOption = <T,>(opt: Option.Option<T>, label: string) =>
|
|
51
|
+
Effect.gen(function* () {
|
|
52
|
+
if (Option.isSome(opt)) {
|
|
53
|
+
yield* Effect.log(`${label}: Some(${opt.value})`);
|
|
54
|
+
} else {
|
|
55
|
+
yield* Effect.log(`${label}: None`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
yield* displayOption(someValue, "someValue");
|
|
60
|
+
yield* displayOption(noneValue, "noneValue");
|
|
61
|
+
|
|
62
|
+
// Example 2: Creating from nullable values
|
|
63
|
+
console.log(`\n[2] Converting nullable to Option:\n`);
|
|
64
|
+
|
|
65
|
+
const possiblyNull = (shouldExist: boolean): string | null =>
|
|
66
|
+
shouldExist ? "found" : null;
|
|
67
|
+
|
|
68
|
+
const toOption = (value: string | null | undefined): Option.Option<string> =>
|
|
69
|
+
value ? Option.some(value) : Option.none();
|
|
70
|
+
|
|
71
|
+
const opt1 = toOption(possiblyNull(true));
|
|
72
|
+
const opt2 = toOption(possiblyNull(false));
|
|
73
|
+
|
|
74
|
+
yield* displayOption(opt1, "toOption(found)");
|
|
75
|
+
yield* displayOption(opt2, "toOption(null)");
|
|
76
|
+
|
|
77
|
+
// Example 3: Pattern matching on Option
|
|
78
|
+
console.log(`\n[3] Pattern matching with match():\n`);
|
|
79
|
+
|
|
80
|
+
const userId: Option.Option<string> = Option.some("user-123");
|
|
81
|
+
|
|
82
|
+
const message = Option.match(userId, {
|
|
83
|
+
onSome: (id) => `User ID: ${id}`,
|
|
84
|
+
onNone: () => "No user found",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
yield* Effect.log(`[MATCH] ${message}`);
|
|
88
|
+
|
|
89
|
+
const emptyUserId: Option.Option<string> = Option.none();
|
|
90
|
+
|
|
91
|
+
const emptyMessage = Option.match(emptyUserId, {
|
|
92
|
+
onSome: (id) => `User ID: ${id}`,
|
|
93
|
+
onNone: () => "No user found",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
yield* Effect.log(`[MATCH] ${emptyMessage}\n`);
|
|
97
|
+
|
|
98
|
+
// Example 4: Transforming with map
|
|
99
|
+
console.log(`[4] Transforming values with map():\n`);
|
|
100
|
+
|
|
101
|
+
const userCount: Option.Option<number> = Option.some(42);
|
|
102
|
+
|
|
103
|
+
const doubled = Option.map(userCount, (count) => count * 2);
|
|
104
|
+
|
|
105
|
+
yield* displayOption(doubled, "doubled");
|
|
106
|
+
|
|
107
|
+
// Chaining maps
|
|
108
|
+
const email: Option.Option<string> = Option.some("user@example.com");
|
|
109
|
+
|
|
110
|
+
const domain = Option.map(email, (e) =>
|
|
111
|
+
e.split("@")[1] ?? "unknown"
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
yield* displayOption(domain, "email domain");
|
|
115
|
+
|
|
116
|
+
// Example 5: Chaining with flatMap
|
|
117
|
+
console.log(`\n[5] Chaining operations with flatMap():\n`);
|
|
118
|
+
|
|
119
|
+
const findUser = (id: string): Option.Option<User> =>
|
|
120
|
+
id === "user-1"
|
|
121
|
+
? Option.some({ id, name: "Alice", email: "alice@example.com" })
|
|
122
|
+
: Option.none();
|
|
123
|
+
|
|
124
|
+
const getProfile = (userId: string): Option.Option<Profile> =>
|
|
125
|
+
userId === "user-1"
|
|
126
|
+
? Option.some({ bio: "Developer", website: "alice.dev" })
|
|
127
|
+
: Option.none();
|
|
128
|
+
|
|
129
|
+
const userId2 = Option.some("user-1");
|
|
130
|
+
|
|
131
|
+
// Chained operations: userId -> user -> profile
|
|
132
|
+
const profileChain = Option.flatMap(userId2, (id) =>
|
|
133
|
+
Option.flatMap(findUser(id), (user) =>
|
|
134
|
+
getProfile(user.id)
|
|
135
|
+
)
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const profileResult = Option.match(profileChain, {
|
|
139
|
+
onSome: (profile) => `Bio: ${profile.bio}, Website: ${profile.website}`,
|
|
140
|
+
onNone: () => "No profile found",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
yield* Effect.log(`[CHAIN] ${profileResult}\n`);
|
|
144
|
+
|
|
145
|
+
// Example 6: Fallback values with getOrElse
|
|
146
|
+
console.log(`[6] Default values with getOrElse():\n`);
|
|
147
|
+
|
|
148
|
+
const optionalStatus: Option.Option<string> = Option.none();
|
|
149
|
+
|
|
150
|
+
const status = Option.getOrElse(optionalStatus, () => "unknown");
|
|
151
|
+
|
|
152
|
+
yield* Effect.log(`[DEFAULT] Status: ${status}`);
|
|
153
|
+
|
|
154
|
+
// Real value
|
|
155
|
+
const knownStatus: Option.Option<string> = Option.some("active");
|
|
156
|
+
|
|
157
|
+
const realStatus = Option.getOrElse(knownStatus, () => "unknown");
|
|
158
|
+
|
|
159
|
+
yield* Effect.log(`[VALUE] Status: ${realStatus}\n`);
|
|
160
|
+
|
|
161
|
+
// Example 7: Filter with predicate
|
|
162
|
+
console.log(`[7] Filtering with conditions:\n`);
|
|
163
|
+
|
|
164
|
+
const ageOption: Option.Option<number> = Option.some(25);
|
|
165
|
+
|
|
166
|
+
const isAdult = Option.filter(ageOption, (age) => age >= 18);
|
|
167
|
+
|
|
168
|
+
yield* displayOption(isAdult, "Adult check (25)");
|
|
169
|
+
|
|
170
|
+
const ageOption2: Option.Option<number> = Option.some(15);
|
|
171
|
+
|
|
172
|
+
const isAdult2 = Option.filter(ageOption2, (age) => age >= 18);
|
|
173
|
+
|
|
174
|
+
yield* displayOption(isAdult2, "Adult check (15)");
|
|
175
|
+
|
|
176
|
+
// Example 8: Multiple Options (all present?)
|
|
177
|
+
console.log(`\n[8] Combining multiple Options:\n`);
|
|
178
|
+
|
|
179
|
+
const firstName: Option.Option<string> = Option.some("John");
|
|
180
|
+
const lastName: Option.Option<string> = Option.some("Doe");
|
|
181
|
+
const middleName: Option.Option<string> = Option.none();
|
|
182
|
+
|
|
183
|
+
// All three present?
|
|
184
|
+
const allPresent = Option.all([firstName, lastName, middleName]);
|
|
185
|
+
|
|
186
|
+
yield* displayOption(allPresent, "All present");
|
|
187
|
+
|
|
188
|
+
// Just two
|
|
189
|
+
const twoPresent = Option.all([firstName, lastName]);
|
|
190
|
+
|
|
191
|
+
yield* displayOption(twoPresent, "Two present");
|
|
192
|
+
|
|
193
|
+
// Example 9: Converting Option to Error
|
|
194
|
+
console.log(`\n[9] Converting Option to Result/Error:\n`);
|
|
195
|
+
|
|
196
|
+
const optionalConfig: Option.Option<{ apiKey: string }> = Option.none();
|
|
197
|
+
|
|
198
|
+
const configOrError = Option.match(optionalConfig, {
|
|
199
|
+
onSome: (config) => config,
|
|
200
|
+
onNone: () => {
|
|
201
|
+
throw new Error("Configuration not found");
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// In real code, would catch error
|
|
206
|
+
const result = Option.match(optionalConfig, {
|
|
207
|
+
onSome: (config) => ({ success: true, value: config }),
|
|
208
|
+
onNone: () => ({ success: false, error: "config-not-found" }),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
yield* Effect.log(`[CONVERT] ${JSON.stringify(result)}\n`);
|
|
212
|
+
|
|
213
|
+
// Example 10: Option in business logic
|
|
214
|
+
console.log(`[10] Practical: Optional user settings:\n`);
|
|
215
|
+
|
|
216
|
+
const userSettings: Option.Option<{
|
|
217
|
+
theme: string;
|
|
218
|
+
notifications: boolean;
|
|
219
|
+
}> = Option.some({
|
|
220
|
+
theme: "dark",
|
|
221
|
+
notifications: true,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const getTheme = Option.map(userSettings, (s) => s.theme);
|
|
225
|
+
const theme = Option.getOrElse(getTheme, () => "light"); // Default
|
|
226
|
+
|
|
227
|
+
yield* Effect.log(`[SETTING] Theme: ${theme}`);
|
|
228
|
+
|
|
229
|
+
// No settings
|
|
230
|
+
const noSettings: Option.Option<{ theme: string; notifications: boolean }> =
|
|
231
|
+
Option.none();
|
|
232
|
+
|
|
233
|
+
const noTheme = Option.map(noSettings, (s) => s.theme);
|
|
234
|
+
const defaultTheme = Option.getOrElse(noTheme, () => "light");
|
|
235
|
+
|
|
236
|
+
yield* Effect.log(`[DEFAULT] Theme: ${defaultTheme}`);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
Effect.runPromise(program);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
**Rationale:**
|
|
245
|
+
|
|
246
|
+
Option enables null-safe programming:
|
|
247
|
+
|
|
248
|
+
- **Some(value)**: Value exists
|
|
249
|
+
- **None**: Value doesn't exist
|
|
250
|
+
- **Pattern matching**: Handle both cases
|
|
251
|
+
- **Chaining**: Compose operations safely
|
|
252
|
+
- **Fallbacks**: Default values
|
|
253
|
+
- **Conversions**: Option ↔ Error
|
|
254
|
+
|
|
255
|
+
Pattern: Use `Option.isSome()`, `Option.isNone()`, `match()`, `map()`, `flatMap()`
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
Null/undefined causes widespread bugs:
|
|
261
|
+
|
|
262
|
+
**Problem 1: Billion-dollar mistake**
|
|
263
|
+
- Tony Hoare invented null in ALGOL in 1965
|
|
264
|
+
- Created "billion-dollar mistake"
|
|
265
|
+
- 90% of security vulnerabilities involve null handling
|
|
266
|
+
|
|
267
|
+
**Problem 2: Undefined behavior**
|
|
268
|
+
- `user.profile.name` - any property could be null
|
|
269
|
+
- Runtime error: "Cannot read property 'name' of undefined"
|
|
270
|
+
- No compile-time warning
|
|
271
|
+
- Production crash
|
|
272
|
+
|
|
273
|
+
**Problem 3: Silent failures**
|
|
274
|
+
- Function returns null on failure
|
|
275
|
+
- Caller doesn't check
|
|
276
|
+
- Uses null as if it's a value
|
|
277
|
+
- Corrupts state downstream
|
|
278
|
+
|
|
279
|
+
**Problem 4: Conditional hell**
|
|
280
|
+
```javascript
|
|
281
|
+
if (user !== null && user.profile !== null && user.profile.name !== null) {
|
|
282
|
+
// Do thing
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Solutions:
|
|
287
|
+
|
|
288
|
+
**Option type**:
|
|
289
|
+
- `Some(value)` = value exists
|
|
290
|
+
- `None` = value doesn't exist
|
|
291
|
+
- Type system forces checking
|
|
292
|
+
- No silent null checks possible
|
|
293
|
+
|
|
294
|
+
**Pattern matching**:
|
|
295
|
+
- `Option.match()`
|
|
296
|
+
- Handle both cases explicitly
|
|
297
|
+
- Compiler warns if you miss one
|
|
298
|
+
|
|
299
|
+
**Chaining**:
|
|
300
|
+
- `option.map().flatMap().match()`
|
|
301
|
+
- Pipeline of operations
|
|
302
|
+
- Null-safe by design
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
## 🟠 Advanced Patterns
|
|
310
|
+
|
|
311
|
+
### Optional Pattern 2: Optional Chaining and Composition
|
|
312
|
+
|
|
313
|
+
**Rule:** Use Option combinators (map, flatMap, ap) to compose operations that may fail, creating readable and maintainable pipelines.
|
|
314
|
+
|
|
315
|
+
**Good Example:**
|
|
316
|
+
|
|
317
|
+
This example demonstrates optional chaining patterns.
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
import { Effect, Option, pipe } from "effect";
|
|
321
|
+
|
|
322
|
+
interface User {
|
|
323
|
+
id: string;
|
|
324
|
+
name: string;
|
|
325
|
+
email: string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface Profile {
|
|
329
|
+
bio: string;
|
|
330
|
+
website?: string;
|
|
331
|
+
avatar?: string;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
interface Settings {
|
|
335
|
+
theme: "light" | "dark";
|
|
336
|
+
notifications: boolean;
|
|
337
|
+
language: string;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const program = Effect.gen(function* () {
|
|
341
|
+
console.log(`\n[OPTIONAL CHAINING] Composing Option operations\n`);
|
|
342
|
+
|
|
343
|
+
// Example 1: Simple chain with map
|
|
344
|
+
console.log(`[1] Chaining transformations with map():\n`);
|
|
345
|
+
|
|
346
|
+
const userId: Option.Option<string> = Option.some("user-42");
|
|
347
|
+
|
|
348
|
+
const userDisplayId = Option.map(userId, (id) => `User#${id}`);
|
|
349
|
+
|
|
350
|
+
const idMessage = Option.match(userDisplayId, {
|
|
351
|
+
onSome: (display) => display,
|
|
352
|
+
onNone: () => "No user ID",
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
yield* Effect.log(`[CHAIN 1] ${idMessage}`);
|
|
356
|
+
|
|
357
|
+
// Chained maps
|
|
358
|
+
const email: Option.Option<string> = Option.some("alice@example.com");
|
|
359
|
+
|
|
360
|
+
const emailParts = pipe(
|
|
361
|
+
email,
|
|
362
|
+
Option.map((e) => e.toLowerCase()),
|
|
363
|
+
Option.map((e) => e.split("@")),
|
|
364
|
+
Option.map((parts) => parts[0]) // username
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const username = Option.getOrElse(emailParts, () => "unknown");
|
|
368
|
+
|
|
369
|
+
yield* Effect.log(`[USERNAME] ${username}\n`);
|
|
370
|
+
|
|
371
|
+
// Example 2: FlatMap for chaining operations that return Option
|
|
372
|
+
console.log(`[2] Chaining operations with flatMap():\n`);
|
|
373
|
+
|
|
374
|
+
const findUser = (id: string): Option.Option<User> =>
|
|
375
|
+
id === "user-42"
|
|
376
|
+
? Option.some({
|
|
377
|
+
id,
|
|
378
|
+
name: "Alice",
|
|
379
|
+
email: "alice@example.com",
|
|
380
|
+
})
|
|
381
|
+
: Option.none();
|
|
382
|
+
|
|
383
|
+
const getProfile = (userId: string): Option.Option<Profile> =>
|
|
384
|
+
userId === "user-42"
|
|
385
|
+
? Option.some({
|
|
386
|
+
bio: "Software engineer",
|
|
387
|
+
website: "alice.dev",
|
|
388
|
+
avatar: "https://example.com/avatar.jpg",
|
|
389
|
+
})
|
|
390
|
+
: Option.none();
|
|
391
|
+
|
|
392
|
+
const userProfile = pipe(
|
|
393
|
+
Option.some("user-42"),
|
|
394
|
+
Option.flatMap((id) => findUser(id)),
|
|
395
|
+
Option.flatMap((user) => getProfile(user.id))
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const profileInfo = Option.match(userProfile, {
|
|
399
|
+
onSome: (profile) => `Bio: ${profile.bio}, Website: ${profile.website}`,
|
|
400
|
+
onNone: () => "Profile not found",
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
yield* Effect.log(`[PROFILE] ${profileInfo}\n`);
|
|
404
|
+
|
|
405
|
+
// Example 3: Complex pipeline
|
|
406
|
+
console.log(`[3] Complex pipeline (user → profile → settings → theme):\n`);
|
|
407
|
+
|
|
408
|
+
const getSettings = (userId: string): Option.Option<Settings> =>
|
|
409
|
+
userId === "user-42"
|
|
410
|
+
? Option.some({
|
|
411
|
+
theme: "dark",
|
|
412
|
+
notifications: true,
|
|
413
|
+
language: "en",
|
|
414
|
+
})
|
|
415
|
+
: Option.none();
|
|
416
|
+
|
|
417
|
+
const userTheme = pipe(
|
|
418
|
+
Option.some("user-42"),
|
|
419
|
+
Option.flatMap((id) => findUser(id)),
|
|
420
|
+
Option.flatMap((user) => getSettings(user.id)),
|
|
421
|
+
Option.map((settings) => settings.theme)
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const theme = Option.getOrElse(userTheme, () => "light");
|
|
425
|
+
|
|
426
|
+
yield* Effect.log(`[THEME] ${theme}`);
|
|
427
|
+
|
|
428
|
+
// Even if any step is None, result is None
|
|
429
|
+
const invalidUserTheme = pipe(
|
|
430
|
+
Option.some("invalid-user"),
|
|
431
|
+
Option.flatMap((id) => findUser(id)),
|
|
432
|
+
Option.flatMap((user) => getSettings(user.id)),
|
|
433
|
+
Option.map((settings) => settings.theme)
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const invalidTheme = Option.getOrElse(invalidUserTheme, () => "light");
|
|
437
|
+
|
|
438
|
+
yield* Effect.log(`[DEFAULT THEME] ${invalidTheme}\n`);
|
|
439
|
+
|
|
440
|
+
// Example 4: Apply (ap) for combining independent Options
|
|
441
|
+
console.log(`[4] Combining values with ap():\n`);
|
|
442
|
+
|
|
443
|
+
const firstName: Option.Option<string> = Option.some("John");
|
|
444
|
+
const lastName: Option.Option<string> = Option.some("Doe");
|
|
445
|
+
|
|
446
|
+
// Create a function wrapped in Option
|
|
447
|
+
const combineNames = (first: string) => (last: string) =>
|
|
448
|
+
`${first} ${last}`;
|
|
449
|
+
|
|
450
|
+
const fullName = pipe(
|
|
451
|
+
Option.some(combineNames),
|
|
452
|
+
Option.ap(firstName),
|
|
453
|
+
Option.ap(lastName)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const name = Option.getOrElse(fullName, () => "Unknown");
|
|
457
|
+
|
|
458
|
+
yield* Effect.log(`[COMBINED] ${name}`);
|
|
459
|
+
|
|
460
|
+
// If any is None
|
|
461
|
+
const noLastName: Option.Option<string> = Option.none();
|
|
462
|
+
|
|
463
|
+
const incompleteName = pipe(
|
|
464
|
+
Option.some(combineNames),
|
|
465
|
+
Option.ap(firstName),
|
|
466
|
+
Option.ap(noLastName)
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const incompleteFull = Option.getOrElse(incompleteName, () => "Incomplete");
|
|
470
|
+
|
|
471
|
+
yield* Effect.log(`[INCOMPLETE] ${incompleteFull}\n`);
|
|
472
|
+
|
|
473
|
+
// Example 5: Traverse for mapping over collections
|
|
474
|
+
console.log(`[5] Working with collections (traverse):\n`);
|
|
475
|
+
|
|
476
|
+
const userIds: string[] = ["user-42", "user-99", "user-1"];
|
|
477
|
+
|
|
478
|
+
// Try to load all users
|
|
479
|
+
const allUsers = Option.all(
|
|
480
|
+
userIds.map((id) => findUser(id))
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const usersMessage = Option.match(allUsers, {
|
|
484
|
+
onSome: (users) => `Loaded ${users.length} users`,
|
|
485
|
+
onNone: () => "Some users not found",
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
yield* Effect.log(`[TRAVERSE] ${usersMessage}\n`);
|
|
489
|
+
|
|
490
|
+
// Example 6: Or/recovery with multiple options
|
|
491
|
+
console.log(`[6] Fallback chains with orElse():\n`);
|
|
492
|
+
|
|
493
|
+
const getPrimaryEmail = (): Option.Option<string> => Option.none();
|
|
494
|
+
const getSecondaryEmail = (): Option.Option<string> =>
|
|
495
|
+
Option.some("backup@example.com");
|
|
496
|
+
const getTertiaryEmail = (): Option.Option<string> =>
|
|
497
|
+
Option.some("tertiary@example.com");
|
|
498
|
+
|
|
499
|
+
const email1 = pipe(
|
|
500
|
+
getPrimaryEmail(),
|
|
501
|
+
Option.orElse(() => getSecondaryEmail()),
|
|
502
|
+
Option.orElse(() => getTertiaryEmail())
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
const contactEmail = Option.getOrElse(email1, () => "no-email@example.com");
|
|
506
|
+
|
|
507
|
+
yield* Effect.log(`[FALLBACK] Using email: ${contactEmail}\n`);
|
|
508
|
+
|
|
509
|
+
// Example 7: Filtering options
|
|
510
|
+
console.log(`[7] Filtering with predicates:\n`);
|
|
511
|
+
|
|
512
|
+
const age: Option.Option<number> = Option.some(25);
|
|
513
|
+
|
|
514
|
+
const canVote = pipe(
|
|
515
|
+
age,
|
|
516
|
+
Option.filter((a) => a >= 18)
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const voteStatus = Option.match(canVote, {
|
|
520
|
+
onSome: () => "Can vote",
|
|
521
|
+
onNone: () => "Too young to vote",
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
yield* Effect.log(`[FILTER] ${voteStatus}`);
|
|
525
|
+
|
|
526
|
+
// Multiple filters in chain
|
|
527
|
+
const score: Option.Option<number> = Option.some(85);
|
|
528
|
+
|
|
529
|
+
const isAGrade = pipe(
|
|
530
|
+
score,
|
|
531
|
+
Option.filter((s) => s >= 80),
|
|
532
|
+
Option.filter((s) => s < 90)
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const grade = Option.match(isAGrade, {
|
|
536
|
+
onSome: () => "Grade A",
|
|
537
|
+
onNone: () => "Not in A range",
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
yield* Effect.log(`[GRADES] ${grade}\n`);
|
|
541
|
+
|
|
542
|
+
// Example 8: Practical: Database query chain
|
|
543
|
+
console.log(`[8] Real-world: Database record chain:\n`);
|
|
544
|
+
|
|
545
|
+
const getRecord = (id: string): Option.Option<{ data: string; nested: { value: number } }> =>
|
|
546
|
+
id === "rec-1"
|
|
547
|
+
? Option.some({
|
|
548
|
+
data: "content",
|
|
549
|
+
nested: { value: 42 },
|
|
550
|
+
})
|
|
551
|
+
: Option.none();
|
|
552
|
+
|
|
553
|
+
const recordValue = pipe(
|
|
554
|
+
Option.some("rec-1"),
|
|
555
|
+
Option.flatMap((id) => getRecord(id)),
|
|
556
|
+
Option.map((rec) => rec.nested),
|
|
557
|
+
Option.map((nested) => nested.value),
|
|
558
|
+
Option.map((value) => value * 2)
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
const finalValue = Option.getOrElse(recordValue, () => 0);
|
|
562
|
+
|
|
563
|
+
yield* Effect.log(`[VALUE] ${finalValue}`);
|
|
564
|
+
|
|
565
|
+
// Missing record
|
|
566
|
+
const missingValue = pipe(
|
|
567
|
+
Option.some("rec-999"),
|
|
568
|
+
Option.flatMap((id) => getRecord(id)),
|
|
569
|
+
Option.map((rec) => rec.nested),
|
|
570
|
+
Option.map((nested) => nested.value),
|
|
571
|
+
Option.map((value) => value * 2)
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
const defaultValue = Option.getOrElse(missingValue, () => 0);
|
|
575
|
+
|
|
576
|
+
yield* Effect.log(`[DEFAULT] ${defaultValue}\n`);
|
|
577
|
+
|
|
578
|
+
// Example 9: Conditional chaining
|
|
579
|
+
console.log(`[9] Conditional paths:\n`);
|
|
580
|
+
|
|
581
|
+
const loadUserWithFallback = (id: string) =>
|
|
582
|
+
pipe(
|
|
583
|
+
findUser(id),
|
|
584
|
+
Option.flatMap((user) =>
|
|
585
|
+
// Only get premium features if user exists
|
|
586
|
+
user.name.includes("Alice")
|
|
587
|
+
? Option.some({ ...user, isPremium: true })
|
|
588
|
+
: Option.none()
|
|
589
|
+
),
|
|
590
|
+
Option.orElse(() =>
|
|
591
|
+
// Fallback: return basic user
|
|
592
|
+
findUser(id)
|
|
593
|
+
)
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
const result1 = loadUserWithFallback("user-42");
|
|
597
|
+
const result2 = loadUserWithFallback("user-99");
|
|
598
|
+
|
|
599
|
+
yield* Effect.log(
|
|
600
|
+
`[CONDITIONAL 1] ${Option.match(result1, { onSome: (u) => `${u.name}`, onNone: () => "Not found" })}`
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
yield* Effect.log(
|
|
604
|
+
`[CONDITIONAL 2] ${Option.match(result2, { onSome: (u) => `${u.name}`, onNone: () => "Not found" })}`
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
Effect.runPromise(program);
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
**Rationale:**
|
|
614
|
+
|
|
615
|
+
Option chaining enables elegant data flows:
|
|
616
|
+
|
|
617
|
+
- **map**: Transform value if present
|
|
618
|
+
- **flatMap**: Chain operations that return Option
|
|
619
|
+
- **ap**: Apply functions wrapped in Option
|
|
620
|
+
- **traverse**: Map over collections with Option
|
|
621
|
+
- **composition**: Combine multiple chains
|
|
622
|
+
- **recovery**: Provide fallbacks
|
|
623
|
+
|
|
624
|
+
Pattern: Use `Option.map()`, `flatMap()`, `ap()`, pipe operators
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
Nested option handling becomes complex:
|
|
630
|
+
|
|
631
|
+
**Problem 1: Pyramid of doom**
|
|
632
|
+
```typescript
|
|
633
|
+
if (user !== null) {
|
|
634
|
+
if (user.profile !== null) {
|
|
635
|
+
if (user.profile.preferences !== null) {
|
|
636
|
+
if (user.profile.preferences.theme !== null) {
|
|
637
|
+
// Finally do thing
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
**Problem 2: Repeated null checks**
|
|
645
|
+
- Every step needs its own check
|
|
646
|
+
- Code duplicates
|
|
647
|
+
- Hard to refactor
|
|
648
|
+
- Bugs easy to introduce
|
|
649
|
+
|
|
650
|
+
**Problem 3: Logic scattered**
|
|
651
|
+
- Transformation logic mixed with null checks
|
|
652
|
+
- Hard to understand intent
|
|
653
|
+
- Error-prone
|
|
654
|
+
|
|
655
|
+
Solutions:
|
|
656
|
+
|
|
657
|
+
**Option chaining**:
|
|
658
|
+
- `None` flows through automatically
|
|
659
|
+
- Transform only if `Some`
|
|
660
|
+
- No intermediate checks needed
|
|
661
|
+
|
|
662
|
+
**Composition**:
|
|
663
|
+
- Combine functions cleanly
|
|
664
|
+
- Separate concerns
|
|
665
|
+
- Reusable pieces
|
|
666
|
+
|
|
667
|
+
**Fallbacks**:
|
|
668
|
+
- `orElse()` for recovery
|
|
669
|
+
- Chain multiple alternatives
|
|
670
|
+
- Graceful degradation
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"outDir": "dist",
|
|
14
|
+
"rootDir": ".",
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/katas/*": ["../../../katas/*"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": ["katas/**/*.ts"]
|
|
20
|
+
}
|
package/vitest.config.ts
ADDED