@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.
Files changed (149) hide show
  1. package/DOJO.md +22 -0
  2. package/dojo.json +50 -0
  3. package/katas/001-hello-effect/SENSEI.md +72 -0
  4. package/katas/001-hello-effect/solution.test.ts +35 -0
  5. package/katas/001-hello-effect/solution.ts +16 -0
  6. package/katas/002-transform-with-map/SENSEI.md +72 -0
  7. package/katas/002-transform-with-map/solution.test.ts +33 -0
  8. package/katas/002-transform-with-map/solution.ts +16 -0
  9. package/katas/003-generator-pipelines/SENSEI.md +72 -0
  10. package/katas/003-generator-pipelines/solution.test.ts +40 -0
  11. package/katas/003-generator-pipelines/solution.ts +29 -0
  12. package/katas/004-flatmap-and-chaining/SENSEI.md +80 -0
  13. package/katas/004-flatmap-and-chaining/solution.test.ts +34 -0
  14. package/katas/004-flatmap-and-chaining/solution.ts +18 -0
  15. package/katas/005-pipe-composition/SENSEI.md +81 -0
  16. package/katas/005-pipe-composition/solution.test.ts +41 -0
  17. package/katas/005-pipe-composition/solution.ts +19 -0
  18. package/katas/006-handle-errors/SENSEI.md +86 -0
  19. package/katas/006-handle-errors/solution.test.ts +53 -0
  20. package/katas/006-handle-errors/solution.ts +30 -0
  21. package/katas/007-tagged-errors/SENSEI.md +79 -0
  22. package/katas/007-tagged-errors/solution.test.ts +82 -0
  23. package/katas/007-tagged-errors/solution.ts +37 -0
  24. package/katas/008-error-patterns/SENSEI.md +89 -0
  25. package/katas/008-error-patterns/solution.test.ts +41 -0
  26. package/katas/008-error-patterns/solution.ts +38 -0
  27. package/katas/009-option-type/SENSEI.md +96 -0
  28. package/katas/009-option-type/solution.test.ts +49 -0
  29. package/katas/009-option-type/solution.ts +26 -0
  30. package/katas/010-either-and-exit/SENSEI.md +86 -0
  31. package/katas/010-either-and-exit/solution.test.ts +33 -0
  32. package/katas/010-either-and-exit/solution.ts +17 -0
  33. package/katas/011-services-and-context/SENSEI.md +82 -0
  34. package/katas/011-services-and-context/solution.test.ts +23 -0
  35. package/katas/011-services-and-context/solution.ts +17 -0
  36. package/katas/012-layers/SENSEI.md +73 -0
  37. package/katas/012-layers/solution.test.ts +23 -0
  38. package/katas/012-layers/solution.ts +26 -0
  39. package/katas/013-testing-effects/SENSEI.md +88 -0
  40. package/katas/013-testing-effects/solution.test.ts +41 -0
  41. package/katas/013-testing-effects/solution.ts +20 -0
  42. package/katas/014-schema-basics/SENSEI.md +81 -0
  43. package/katas/014-schema-basics/solution.test.ts +35 -0
  44. package/katas/014-schema-basics/solution.ts +25 -0
  45. package/katas/015-domain-modeling/SENSEI.md +85 -0
  46. package/katas/015-domain-modeling/solution.test.ts +46 -0
  47. package/katas/015-domain-modeling/solution.ts +42 -0
  48. package/katas/016-retry-and-schedule/SENSEI.md +72 -0
  49. package/katas/016-retry-and-schedule/solution.test.ts +26 -0
  50. package/katas/016-retry-and-schedule/solution.ts +23 -0
  51. package/katas/017-parallel-effects/SENSEI.md +70 -0
  52. package/katas/017-parallel-effects/solution.test.ts +33 -0
  53. package/katas/017-parallel-effects/solution.ts +17 -0
  54. package/katas/018-race-and-timeout/SENSEI.md +75 -0
  55. package/katas/018-race-and-timeout/solution.test.ts +30 -0
  56. package/katas/018-race-and-timeout/solution.ts +27 -0
  57. package/katas/019-ref-and-state/SENSEI.md +72 -0
  58. package/katas/019-ref-and-state/solution.test.ts +29 -0
  59. package/katas/019-ref-and-state/solution.ts +16 -0
  60. package/katas/020-fibers/SENSEI.md +80 -0
  61. package/katas/020-fibers/solution.test.ts +23 -0
  62. package/katas/020-fibers/solution.ts +23 -0
  63. package/katas/021-acquire-release/SENSEI.md +57 -0
  64. package/katas/021-acquire-release/solution.test.ts +23 -0
  65. package/katas/021-acquire-release/solution.ts +22 -0
  66. package/katas/022-scoped-layers/SENSEI.md +52 -0
  67. package/katas/022-scoped-layers/solution.test.ts +35 -0
  68. package/katas/022-scoped-layers/solution.ts +19 -0
  69. package/katas/023-resource-patterns/SENSEI.md +52 -0
  70. package/katas/023-resource-patterns/solution.test.ts +20 -0
  71. package/katas/023-resource-patterns/solution.ts +13 -0
  72. package/katas/024-streams-basics/SENSEI.md +61 -0
  73. package/katas/024-streams-basics/solution.test.ts +30 -0
  74. package/katas/024-streams-basics/solution.ts +16 -0
  75. package/katas/025-stream-operations/SENSEI.md +59 -0
  76. package/katas/025-stream-operations/solution.test.ts +26 -0
  77. package/katas/025-stream-operations/solution.ts +17 -0
  78. package/katas/026-combining-streams/SENSEI.md +54 -0
  79. package/katas/026-combining-streams/solution.test.ts +20 -0
  80. package/katas/026-combining-streams/solution.ts +16 -0
  81. package/katas/027-data-pipelines/SENSEI.md +58 -0
  82. package/katas/027-data-pipelines/solution.test.ts +22 -0
  83. package/katas/027-data-pipelines/solution.ts +16 -0
  84. package/katas/028-logging-and-spans/SENSEI.md +58 -0
  85. package/katas/028-logging-and-spans/solution.test.ts +50 -0
  86. package/katas/028-logging-and-spans/solution.ts +20 -0
  87. package/katas/029-http-client/SENSEI.md +59 -0
  88. package/katas/029-http-client/solution.test.ts +49 -0
  89. package/katas/029-http-client/solution.ts +24 -0
  90. package/katas/030-capstone/SENSEI.md +63 -0
  91. package/katas/030-capstone/solution.test.ts +67 -0
  92. package/katas/030-capstone/solution.ts +55 -0
  93. package/katas/031-config-and-environment/SENSEI.md +77 -0
  94. package/katas/031-config-and-environment/solution.test.ts +38 -0
  95. package/katas/031-config-and-environment/solution.ts +11 -0
  96. package/katas/032-cause-and-defects/SENSEI.md +90 -0
  97. package/katas/032-cause-and-defects/solution.test.ts +50 -0
  98. package/katas/032-cause-and-defects/solution.ts +23 -0
  99. package/katas/033-pattern-matching/SENSEI.md +86 -0
  100. package/katas/033-pattern-matching/solution.test.ts +36 -0
  101. package/katas/033-pattern-matching/solution.ts +28 -0
  102. package/katas/034-deferred-and-coordination/SENSEI.md +85 -0
  103. package/katas/034-deferred-and-coordination/solution.test.ts +25 -0
  104. package/katas/034-deferred-and-coordination/solution.ts +24 -0
  105. package/katas/035-queue-and-backpressure/SENSEI.md +100 -0
  106. package/katas/035-queue-and-backpressure/solution.test.ts +25 -0
  107. package/katas/035-queue-and-backpressure/solution.ts +21 -0
  108. package/katas/036-schema-advanced/SENSEI.md +81 -0
  109. package/katas/036-schema-advanced/solution.test.ts +55 -0
  110. package/katas/036-schema-advanced/solution.ts +19 -0
  111. package/katas/037-cache-and-memoization/SENSEI.md +73 -0
  112. package/katas/037-cache-and-memoization/solution.test.ts +47 -0
  113. package/katas/037-cache-and-memoization/solution.ts +24 -0
  114. package/katas/038-metrics/SENSEI.md +91 -0
  115. package/katas/038-metrics/solution.test.ts +39 -0
  116. package/katas/038-metrics/solution.ts +23 -0
  117. package/katas/039-managed-runtime/SENSEI.md +75 -0
  118. package/katas/039-managed-runtime/solution.test.ts +29 -0
  119. package/katas/039-managed-runtime/solution.ts +19 -0
  120. package/katas/040-request-batching/SENSEI.md +87 -0
  121. package/katas/040-request-batching/solution.test.ts +56 -0
  122. package/katas/040-request-batching/solution.ts +32 -0
  123. package/package.json +22 -0
  124. package/skills/effect-patterns-building-apis/SKILL.md +2393 -0
  125. package/skills/effect-patterns-building-data-pipelines/SKILL.md +1876 -0
  126. package/skills/effect-patterns-concurrency/SKILL.md +2999 -0
  127. package/skills/effect-patterns-concurrency-getting-started/SKILL.md +351 -0
  128. package/skills/effect-patterns-core-concepts/SKILL.md +3199 -0
  129. package/skills/effect-patterns-domain-modeling/SKILL.md +1385 -0
  130. package/skills/effect-patterns-error-handling/SKILL.md +1212 -0
  131. package/skills/effect-patterns-error-handling-resilience/SKILL.md +179 -0
  132. package/skills/effect-patterns-error-management/SKILL.md +1668 -0
  133. package/skills/effect-patterns-getting-started/SKILL.md +237 -0
  134. package/skills/effect-patterns-making-http-requests/SKILL.md +1756 -0
  135. package/skills/effect-patterns-observability/SKILL.md +1586 -0
  136. package/skills/effect-patterns-platform/SKILL.md +1195 -0
  137. package/skills/effect-patterns-platform-getting-started/SKILL.md +179 -0
  138. package/skills/effect-patterns-project-setup--execution/SKILL.md +233 -0
  139. package/skills/effect-patterns-resource-management/SKILL.md +827 -0
  140. package/skills/effect-patterns-scheduling/SKILL.md +451 -0
  141. package/skills/effect-patterns-scheduling-periodic-tasks/SKILL.md +763 -0
  142. package/skills/effect-patterns-streams/SKILL.md +2052 -0
  143. package/skills/effect-patterns-streams-getting-started/SKILL.md +421 -0
  144. package/skills/effect-patterns-streams-sinks/SKILL.md +1181 -0
  145. package/skills/effect-patterns-testing/SKILL.md +1632 -0
  146. package/skills/effect-patterns-tooling-and-debugging/SKILL.md +1125 -0
  147. package/skills/effect-patterns-value-handling/SKILL.md +676 -0
  148. package/tsconfig.json +20 -0
  149. package/vitest.config.ts +3 -0
package/DOJO.md ADDED
@@ -0,0 +1,22 @@
1
+ # Effect-TS Kata Dojo
2
+
3
+ Hands-on katas for learning Effect-TS. Use `/kata` to begin.
4
+
5
+ ## Teaching Rules
6
+
7
+ **Never give solutions.** Your role is Socratic guide.
8
+
9
+ - Ask questions that steer toward the answer
10
+ - Point to type signatures or API names
11
+ - Narrow scope: "Focus on the first failing test"
12
+ - Never write or show solution code
13
+
14
+ **SENSEI.md is the authority.** Each kata has one. Read it first — it has the Test Map, teaching prompts, and completion guidance. It overrides these defaults.
15
+
16
+ **Concept accuracy.** Only teach APIs the student writes. Test-only APIs (`runSync`, `runSyncExit`) belong to the harness — don't attribute them to the student.
17
+
18
+ **Area introductions.** At area boundaries, invoke the skill listed in SENSEI.md's "Skills" section.
19
+
20
+ ## Style
21
+
22
+ Clean, minimal, encouraging. No walls of text.
package/dojo.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "$schema": "https://dojocho.ai/schema/v1/dojo.json",
3
+ "name": "@dojocho/effect-ts",
4
+ "version": "0.0.1",
5
+ "description": "Master Effect-TS through 40 hands-on katas",
6
+
7
+ "test": "pnpm vitest run {template}",
8
+ "katas": [
9
+ { "template": "katas/001-hello-effect/solution.ts" },
10
+ { "template": "katas/002-transform-with-map/solution.ts" },
11
+ { "template": "katas/003-generator-pipelines/solution.ts" },
12
+ { "template": "katas/004-flatmap-and-chaining/solution.ts" },
13
+ { "template": "katas/005-pipe-composition/solution.ts" },
14
+ { "template": "katas/006-handle-errors/solution.ts" },
15
+ { "template": "katas/007-tagged-errors/solution.ts" },
16
+ { "template": "katas/008-error-patterns/solution.ts" },
17
+ { "template": "katas/009-option-type/solution.ts" },
18
+ { "template": "katas/010-either-and-exit/solution.ts" },
19
+ { "template": "katas/011-services-and-context/solution.ts" },
20
+ { "template": "katas/012-layers/solution.ts" },
21
+ { "template": "katas/013-testing-effects/solution.ts" },
22
+ { "template": "katas/014-schema-basics/solution.ts" },
23
+ { "template": "katas/015-domain-modeling/solution.ts" },
24
+ { "template": "katas/016-retry-and-schedule/solution.ts" },
25
+ { "template": "katas/017-parallel-effects/solution.ts" },
26
+ { "template": "katas/018-race-and-timeout/solution.ts" },
27
+ { "template": "katas/019-ref-and-state/solution.ts" },
28
+ { "template": "katas/020-fibers/solution.ts" },
29
+ { "template": "katas/021-acquire-release/solution.ts" },
30
+ { "template": "katas/022-scoped-layers/solution.ts" },
31
+ { "template": "katas/023-resource-patterns/solution.ts" },
32
+ { "template": "katas/024-streams-basics/solution.ts" },
33
+ { "template": "katas/025-stream-operations/solution.ts" },
34
+ { "template": "katas/026-combining-streams/solution.ts" },
35
+ { "template": "katas/027-data-pipelines/solution.ts" },
36
+ { "template": "katas/028-logging-and-spans/solution.ts" },
37
+ { "template": "katas/029-http-client/solution.ts" },
38
+ { "template": "katas/030-capstone/solution.ts", "test": "pnpm vitest run katas/" },
39
+ { "template": "katas/031-config-and-environment/solution.ts" },
40
+ { "template": "katas/032-cause-and-defects/solution.ts" },
41
+ { "template": "katas/033-pattern-matching/solution.ts" },
42
+ { "template": "katas/034-deferred-and-coordination/solution.ts" },
43
+ { "template": "katas/035-queue-and-backpressure/solution.ts" },
44
+ { "template": "katas/036-schema-advanced/solution.ts" },
45
+ { "template": "katas/037-cache-and-memoization/solution.ts" },
46
+ { "template": "katas/038-metrics/solution.ts" },
47
+ { "template": "katas/039-managed-runtime/solution.ts" },
48
+ { "template": "katas/040-request-batching/solution.ts", "test": "pnpm vitest run katas/" }
49
+ ]
50
+ }
@@ -0,0 +1,72 @@
1
+ # SENSEI — 001 Hello Effect
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Create and run your first Effects.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `hello()` — returns an Effect that succeeds with `"Hello, Effect!"`
12
+ 2. Implement `lazyRandom()` — returns an Effect that lazily produces a random number (0–1)
13
+ 3. Implement `greet(name)` — returns an Effect that succeeds with `"Hello, {name}!"`
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect } from "effect";
19
+
20
+ // Effect.succeed wraps a plain value
21
+ const myEffect = Effect.succeed(42);
22
+
23
+ // Effect.sync wraps a lazy computation
24
+ const lazy = Effect.sync(() => Math.random());
25
+
26
+ // Effect.runSync executes synchronously
27
+ const value = Effect.runSync(myEffect); // 42
28
+ ```
29
+
30
+ ## Prerequisites
31
+
32
+ None — this is the first kata.
33
+
34
+ ## Skills
35
+
36
+ Invoke `effect-patterns-getting-started` before teaching this kata.
37
+
38
+ > **Note**: `Effect.runSync` appears only in tests. The student does NOT write it. Never attribute it to their learning.
39
+
40
+ ## Test Map
41
+
42
+ | Test | Concept | Verifies |
43
+ |------|---------|----------|
44
+ | `hello() succeeds with 'Hello, Effect!'` | `Effect.succeed` | Wrapping a string literal |
45
+ | `lazyRandom() produces a number between 0 and 1` | `Effect.sync` | Lazy computation wrapping `Math.random()` |
46
+ | `lazyRandom() is lazy (re-evaluates on each run)` | `Effect.sync` | Same Effect produces different values across multiple runs |
47
+ | `greet('World') succeeds with 'Hello, World!'` | `Effect.succeed` | Parameterized value wrapping |
48
+ | `greet('Effect') succeeds with 'Hello, Effect!'` | `Effect.succeed` | Parameterized value wrapping |
49
+
50
+ ## Teaching Approach
51
+
52
+ ### Socratic prompts
53
+
54
+ - "What's the difference between wrapping a value directly vs wrapping a function that produces a value?"
55
+ - "If you use `Effect.succeed(Math.random())`, when does `Math.random()` actually run?"
56
+ - "What does the type `Effect.Effect<string>` tell you about what this Effect produces?"
57
+
58
+ ### Common pitfalls
59
+
60
+ 1. **Using `succeed` for `lazyRandom`** — `Effect.succeed(Math.random())` evaluates `Math.random()` immediately when the function is called, not when the Effect is run. The value gets "baked in". Ask: "Try running `lazyRandom()` twice and comparing — are they always different? Why or why not?"
61
+ 2. **Overcomplicating `greet`** — students may try template literals inside `sync`. Nudge: "Does `greet` need laziness? It already has the name parameter."
62
+ 3. **Tackle one function at a time** — get `hello()` passing first, then think about what makes `lazyRandom` different.
63
+
64
+ ## On Completion
65
+
66
+ ### Insight
67
+
68
+ You used two ways to create Effects: `succeed` for values you already have, and `sync` for computations you want to defer. The tests used `Effect.runSync` to execute your Effects — but notice **you** never wrote `runSync`. In Effect, creating a computation and running it are separate concerns. This separation is what makes Effects composable.
69
+
70
+ ### Bridge
71
+
72
+ Now that you can create Effects, the next step is **transforming** them. Kata 002 introduces `Effect.map` and `pipe` — the building blocks for turning one Effect's output into something new without leaving the Effect world.
@@ -0,0 +1,35 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { greet, hello, lazyRandom } from "@/katas/001-hello-effect/solution.js";
4
+
5
+ describe("001 — Hello Effect", () => {
6
+ it("hello() succeeds with 'Hello, Effect!'", () => {
7
+ const result = Effect.runSync(hello());
8
+ expect(result).toBe("Hello, Effect!");
9
+ });
10
+
11
+ it("lazyRandom() produces a number between 0 and 1", () => {
12
+ const result = Effect.runSync(lazyRandom());
13
+ expect(result).toBeTypeOf("number");
14
+ expect(result).toBeGreaterThanOrEqual(0);
15
+ expect(result).toBeLessThan(1);
16
+ });
17
+
18
+ it("lazyRandom() is lazy (re-evaluates on each run)", () => {
19
+ const effect = lazyRandom();
20
+ const results = Array.from({ length: 10 }, () => Effect.runSync(effect));
21
+ const unique = new Set(results);
22
+ // Running the same Effect 10 times should produce multiple distinct values
23
+ expect(unique.size).toBeGreaterThan(1);
24
+ });
25
+
26
+ it("greet('World') succeeds with 'Hello, World!'", () => {
27
+ const result = Effect.runSync(greet("World"));
28
+ expect(result).toBe("Hello, World!");
29
+ });
30
+
31
+ it("greet('Effect') succeeds with 'Hello, Effect!'", () => {
32
+ const result = Effect.runSync(greet("Effect"));
33
+ expect(result).toBe("Hello, Effect!");
34
+ });
35
+ });
@@ -0,0 +1,16 @@
1
+ import { Effect } from "effect";
2
+
3
+ /** Return an Effect that succeeds with "Hello, Effect!" */
4
+ export const hello = (): Effect.Effect<string> => {
5
+ throw new Error("Not implemented");
6
+ };
7
+
8
+ /** Return an Effect that lazily produces a random number (0–1) */
9
+ export const lazyRandom = (): Effect.Effect<number> => {
10
+ throw new Error("Not implemented");
11
+ };
12
+
13
+ /** Return an Effect that succeeds with "Hello, {name}!" */
14
+ export const greet = (name: string): Effect.Effect<string> => {
15
+ throw new Error("Not implemented");
16
+ };
@@ -0,0 +1,72 @@
1
+ # SENSEI — 002 Transform with Map
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Transform Effect values using `map` and `pipe`.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `double(n)` — returns an Effect that succeeds with `n * 2`
12
+ 2. Implement `strlen(s)` — returns an Effect that succeeds with the length of `s`
13
+ 3. Implement `doubleAndFormat(n)` — uses pipe + map to double a number and format as `"Result: {n}"`
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect, pipe } from "effect";
19
+
20
+ // Effect.map transforms the success value
21
+ const doubled = Effect.map(Effect.succeed(21), (n) => n * 2);
22
+
23
+ // pipe lets you compose left-to-right
24
+ const result = pipe(
25
+ Effect.succeed(10),
26
+ Effect.map((n) => n + 1),
27
+ Effect.map((n) => `Value: ${n}`),
28
+ );
29
+ ```
30
+
31
+ ## Prerequisites
32
+
33
+ - **001 Hello Effect** — `Effect.succeed`, `Effect.sync` (creating Effects)
34
+
35
+ > **Note**: `Effect.runSync` appears only in tests. Never attribute it to the user's learning.
36
+
37
+ ## Test Map
38
+
39
+ | Test | Concept | Verifies |
40
+ |------|---------|----------|
41
+ | `double(5) succeeds with 10` | `Effect.succeed` + `Effect.map` | Basic numeric transformation |
42
+ | `double(0) succeeds with 0` | `Effect.map` | Edge case — identity under doubling |
43
+ | `double(-3) succeeds with -6` | `Effect.map` | Negative numbers |
44
+ | `strlen('hello') succeeds with 5` | `Effect.succeed` + `Effect.map` | String-to-number transformation |
45
+ | `strlen('') succeeds with 0` | `Effect.map` | Empty string edge case |
46
+ | `doubleAndFormat(5) succeeds with 'Result: 10'` | `pipe` + `Effect.map` | Chained transformations |
47
+ | `doubleAndFormat(0) succeeds with 'Result: 0'` | `pipe` + `Effect.map` | Chained edge case |
48
+
49
+ ## Teaching Approach
50
+
51
+ ### Socratic prompts
52
+
53
+ - "You know how to create Effects with `succeed`. What if you want to change the value inside without leaving the Effect world?"
54
+ - "What does `Effect.map` return — a plain value or an Effect?"
55
+ - "In `doubleAndFormat`, you need two steps. How does `pipe` help you connect them?"
56
+
57
+ ### Common pitfalls
58
+
59
+ 1. **Returning plain values from map** — `Effect.map(effect, (n) => n * 2)` is correct. Students sometimes try `Effect.succeed(Effect.map(...))` — double-wrapping. Ask: "What does `map` already return?"
60
+ 2. **Skipping pipe for `doubleAndFormat`** — students may try to nest maps or create intermediate variables. Nudge: "Can you express both steps in a single `pipe` chain?"
61
+ 3. **Confusing `pipe` import** — `pipe` comes from `"effect"`, not a separate module. The type signature helps: it takes a value and a series of functions.
62
+ 4. **Start simple** — start with `double` (just `succeed` + `map`), then for `doubleAndFormat` add a second `map` step in the same `pipe`.
63
+
64
+ ## On Completion
65
+
66
+ ### Insight
67
+
68
+ `Effect.map` keeps you inside the Effect world — you never have to "unwrap" the value, transform it, and "re-wrap" it. This is what makes Effects composable: each transformation step produces a new Effect that chains naturally into the next. Think of `pipe` as a conveyor belt where each `map` is a station that modifies the item passing through.
69
+
70
+ ### Bridge
71
+
72
+ Pipe chains with `map` work great when each step is synchronous and infallible. But what happens when a step itself needs to create an Effect — like parsing that might fail? Kata 003 introduces **generators** (`Effect.gen`), giving you an imperative-looking way to sequence multiple Effect steps.
@@ -0,0 +1,33 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { double, doubleAndFormat, strlen } from "@/katas/002-transform-with-map/solution.js";
4
+
5
+ describe("002 — Transform with Map", () => {
6
+ it("double(5) succeeds with 10", () => {
7
+ expect(Effect.runSync(double(5))).toBe(10);
8
+ });
9
+
10
+ it("double(0) succeeds with 0", () => {
11
+ expect(Effect.runSync(double(0))).toBe(0);
12
+ });
13
+
14
+ it("double(-3) succeeds with -6", () => {
15
+ expect(Effect.runSync(double(-3))).toBe(-6);
16
+ });
17
+
18
+ it("strlen('hello') succeeds with 5", () => {
19
+ expect(Effect.runSync(strlen("hello"))).toBe(5);
20
+ });
21
+
22
+ it("strlen('') succeeds with 0", () => {
23
+ expect(Effect.runSync(strlen(""))).toBe(0);
24
+ });
25
+
26
+ it("doubleAndFormat(5) succeeds with 'Result: 10'", () => {
27
+ expect(Effect.runSync(doubleAndFormat(5))).toBe("Result: 10");
28
+ });
29
+
30
+ it("doubleAndFormat(0) succeeds with 'Result: 0'", () => {
31
+ expect(Effect.runSync(doubleAndFormat(0))).toBe("Result: 0");
32
+ });
33
+ });
@@ -0,0 +1,16 @@
1
+ import { Effect } from "effect";
2
+
3
+ /** Return an Effect that succeeds with n * 2 */
4
+ export const double = (n: number): Effect.Effect<number> => {
5
+ throw new Error("Not implemented");
6
+ };
7
+
8
+ /** Return an Effect that succeeds with the length of s */
9
+ export const strlen = (s: string): Effect.Effect<number> => {
10
+ throw new Error("Not implemented");
11
+ };
12
+
13
+ /** Use pipe + Effect.map to double n, then format as "Result: {doubled}" */
14
+ export const doubleAndFormat = (n: number): Effect.Effect<string> => {
15
+ throw new Error("Not implemented");
16
+ };
@@ -0,0 +1,72 @@
1
+ # SENSEI — 003 Generator Pipelines
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Use `Effect.gen` to write imperative-style Effect pipelines with generators.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `fetchAndDouble(n)` — uses `Effect.gen` to get a value, double it, and return it
12
+ 2. Implement `combinedLength(a, b)` — uses `Effect.gen` to get lengths of two strings and add them
13
+ 3. Implement `pipeline(s)` — uses `Effect.gen` to parse an integer string, double it, and format as `"Result: {n}"`; fails with `ParseError` on invalid input
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect } from "effect";
19
+
20
+ const program = Effect.gen(function* () {
21
+ const a = yield* Effect.succeed(10);
22
+ const b = yield* Effect.succeed(20);
23
+ return a + b;
24
+ });
25
+
26
+ // Effect.runSync(program) => 30
27
+ ```
28
+
29
+ ## Prerequisites
30
+
31
+ - **001 Hello Effect** — `Effect.succeed`, `Effect.sync`
32
+ - **002 Transform with Map** — `Effect.map`, `pipe`
33
+
34
+ > **Note**: `Effect.runSync`, `Effect.runSyncExit`, and `Exit.isFailure` appear only in tests. Never attribute them to the user's learning.
35
+
36
+ ## Test Map
37
+
38
+ | Test | Concept | Verifies |
39
+ |------|---------|----------|
40
+ | `fetchAndDouble(5) succeeds with 10` | `Effect.gen` + `yield*` | Basic generator with single yield |
41
+ | `fetchAndDouble(0) succeeds with 0` | `Effect.gen` | Edge case |
42
+ | `combinedLength('hello', 'world') succeeds with 10` | `Effect.gen` + multiple `yield*` | Combining results from multiple Effects |
43
+ | `combinedLength('', 'test') succeeds with 4` | `Effect.gen` | Empty string edge case |
44
+ | `pipeline('5') succeeds with 'Result: 10'` | `Effect.gen` | Multi-step pipeline in generator |
45
+ | `pipeline('0') succeeds with 'Result: 0'` | `Effect.gen` | Edge case |
46
+ | `pipeline('abc') fails with ParseError` | `Effect.fail` | Error handling inside generator |
47
+
48
+ ## Teaching Approach
49
+
50
+ ### Socratic prompts
51
+
52
+ - "In kata 002 you used `pipe` + `map`. What if you want to use an intermediate value in a later step — how would you do that with just `map`?"
53
+ - "What does `yield*` do to an Effect? What type does the variable get?"
54
+ - "Inside `Effect.gen`, if you `yield*` an Effect that fails, what happens to the rest of the generator?"
55
+
56
+ ### Common pitfalls
57
+
58
+ 1. **Forgetting `yield*`** — writing `const x = Effect.succeed(5)` inside a generator gives you an Effect, not the value 5. Ask: "What's the type of `x` without `yield*`?"
59
+ 2. **Using `return` vs `yield*` for the final value** — the last expression in `Effect.gen` should be a plain `return`, not `yield* Effect.succeed(...)`. Nudge: "You can just `return` the computed value directly."
60
+ 3. **ParseError shape** — the `pipeline` function needs to fail with `{ _tag: "ParseError", input: s }`. Students may forget the `input` field. Ask: "What does the `ParseError` interface require?"
61
+ 4. **Checking for NaN** — `parseInt("abc")` returns `NaN`, not an error. Ask: "How do you detect when parsing didn't work?"
62
+ 5. **Break `pipeline` into steps** — parse, check validity, format. Ask: "Which step might fail?" and point to the `ParseError` interface in solution.ts.
63
+
64
+ ## On Completion
65
+
66
+ ### Insight
67
+
68
+ Generators give you imperative-style code that's still fully Effect-powered. Each `yield*` is a suspension point — if any yielded Effect fails, the generator short-circuits, just like `throw` in regular code. You now have two styles: `pipe` chains (functional) and `gen` blocks (imperative). Neither is "better" — use whichever reads more clearly for the situation.
69
+
70
+ ### Bridge
71
+
72
+ Generators and pipe both sequence Effects, but they hide the underlying mechanism: `Effect.flatMap`. Kata 004 makes flatMap explicit, along with `andThen` and `tap` — giving you direct control over how Effects chain together.
@@ -0,0 +1,40 @@
1
+ import { Effect, Exit } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { combinedLength, fetchAndDouble, pipeline } from "@/katas/003-generator-pipelines/solution.js";
4
+
5
+ describe("003 — Generator Pipelines", () => {
6
+ it("fetchAndDouble(5) succeeds with 10", () => {
7
+ expect(Effect.runSync(fetchAndDouble(5))).toBe(10);
8
+ });
9
+
10
+ it("fetchAndDouble(0) succeeds with 0", () => {
11
+ expect(Effect.runSync(fetchAndDouble(0))).toBe(0);
12
+ });
13
+
14
+ it("combinedLength('hello', 'world') succeeds with 10", () => {
15
+ expect(Effect.runSync(combinedLength("hello", "world"))).toBe(10);
16
+ });
17
+
18
+ it("combinedLength('', 'test') succeeds with 4", () => {
19
+ expect(Effect.runSync(combinedLength("", "test"))).toBe(4);
20
+ });
21
+
22
+ it("pipeline('5') succeeds with 'Result: 10'", () => {
23
+ expect(Effect.runSync(pipeline("5"))).toBe("Result: 10");
24
+ });
25
+
26
+ it("pipeline('0') succeeds with 'Result: 0'", () => {
27
+ expect(Effect.runSync(pipeline("0"))).toBe("Result: 0");
28
+ });
29
+
30
+ it("pipeline('abc') fails with ParseError", () => {
31
+ const exit = Effect.runSyncExit(pipeline("abc"));
32
+ expect(Exit.isFailure(exit)).toBe(true);
33
+ if (Exit.isFailure(exit)) {
34
+ expect(exit.cause).toMatchObject({
35
+ _tag: "Fail",
36
+ error: { _tag: "ParseError", input: "abc" },
37
+ });
38
+ }
39
+ });
40
+ });
@@ -0,0 +1,29 @@
1
+ import { Effect } from "effect";
2
+
3
+ export interface ParseError {
4
+ readonly _tag: "ParseError";
5
+ readonly input: string;
6
+ }
7
+
8
+ /** Use Effect.gen to succeed with n * 2 */
9
+ export const fetchAndDouble = (n: number): Effect.Effect<number> => {
10
+ throw new Error("Not implemented");
11
+ };
12
+
13
+ /** Use Effect.gen to get the length of a and b, then return their sum */
14
+ export const combinedLength = (
15
+ a: string,
16
+ b: string,
17
+ ): Effect.Effect<number> => {
18
+ throw new Error("Not implemented");
19
+ };
20
+
21
+ /** Use Effect.gen to:
22
+ * 1. Parse s as an integer (fail with ParseError if invalid)
23
+ * 2. Double the result
24
+ * 3. Return formatted as "Result: {doubled}" */
25
+ export const pipeline = (
26
+ s: string,
27
+ ): Effect.Effect<string, ParseError> => {
28
+ throw new Error("Not implemented");
29
+ };
@@ -0,0 +1,80 @@
1
+ # SENSEI — 004 FlatMap and Chaining
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Chain Effects together using flatMap, andThen, and tap.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `lookupAndGreet(id)` — flatMap a lookup to return a greeting
12
+ 2. Implement `validateAndProcess(input)` — chain validation then processing
13
+ 3. Implement `logAndReturn(value, sideEffects)` — use tap to log a side effect without changing the value
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect } from "effect";
19
+
20
+ // Effect.flatMap chains a dependent Effect
21
+ const result = Effect.succeed(1).pipe(
22
+ Effect.flatMap((n) => Effect.succeed(n + 1))
23
+ );
24
+
25
+ // Effect.andThen sequences two Effects
26
+ const chained = Effect.succeed("hello").pipe(
27
+ Effect.andThen((s) => Effect.succeed(s.toUpperCase()))
28
+ );
29
+
30
+ // Effect.tap runs a side effect without altering the result
31
+ const tapped = Effect.succeed(42).pipe(
32
+ Effect.tap((n) => Effect.sync(() => console.log(n)))
33
+ );
34
+ ```
35
+
36
+ ## Prerequisites
37
+
38
+ - **001 Hello Effect** — `Effect.succeed`, `Effect.sync`
39
+ - **002 Transform with Map** — `Effect.map`, `pipe`
40
+ - **003 Generator Pipelines** — `Effect.gen`, `yield*`, `Effect.fail`
41
+
42
+ > **Note**: `Effect.runSync`, `Effect.runSyncExit`, and `Exit.isFailure` appear only in tests. Never attribute them to the user's learning.
43
+
44
+ ## Test Map
45
+
46
+ | Test | Concept | Verifies |
47
+ |------|---------|----------|
48
+ | `lookupAndGreet(0) succeeds with 'Hello, Alice!'` | `Effect.flatMap` | Chain dependent Effects — success path |
49
+ | `lookupAndGreet(1) succeeds with 'Hello, Bob!'` | `Effect.flatMap` | Chain dependent Effects — different lookup |
50
+ | `lookupAndGreet(99) fails with 'NotFound'` | `Effect.flatMap` + `Effect.fail` | Error propagation in chain |
51
+ | `validateAndProcess('hello') succeeds with 'HELLO'` | `Effect.andThen` | Sequential processing — success |
52
+ | `validateAndProcess('') fails with 'EmptyInput'` | `Effect.andThen` + `Effect.fail` | Validation failure |
53
+ | `logAndReturn records side effect and returns value` | `Effect.tap` + `Effect.sync` | Side effect without value change |
54
+
55
+ ## Teaching Approach
56
+
57
+ ### Socratic prompts
58
+
59
+ - "In `map`, your function returns a plain value. In `flatMap`, your function returns an Effect. Why would you need that?"
60
+ - "What's the difference between `flatMap` and `andThen`? Try looking at their type signatures."
61
+ - "If `tap` doesn't change the value, what's it useful for?"
62
+ - "For `lookupAndGreet`, you need a lookup step that might fail. Can a `map` callback fail? What can?"
63
+
64
+ ### Common pitfalls
65
+
66
+ 1. **Using `map` instead of `flatMap`** — if your callback returns an Effect, you'll get `Effect<Effect<...>>` (nested). Ask: "What type does `map` give you if your function returns an Effect?"
67
+ 2. **Lookup logic** — students may overcomplicate the id-to-name mapping. Nudge: "A simple `if/else` or ternary works. What should happen for id 0? For id 1? For anything else?"
68
+ 3. **`tap` changing the value** — the callback in `tap` runs for its side effect; its return value is ignored (the original value passes through). Ask: "After `tap`, what value does the chain continue with?"
69
+ 4. **`logAndReturn` side effect** — students need to push to the `sideEffects` array. This is a mutation, so use `Effect.sync(() => { ... })` inside `tap`.
70
+ 5. **Start with `lookupAndGreet`** — first create an Effect that looks up the name, then use `flatMap` to turn that name into a greeting Effect.
71
+
72
+ ## On Completion
73
+
74
+ ### Insight
75
+
76
+ `flatMap` is the fundamental chaining operation in Effect. Everything else builds on it: `gen` desugars `yield*` into flatMap calls, `andThen` is a more flexible flatMap, and `map` is flatMap where the callback wraps its return in `succeed` automatically. Understanding flatMap means understanding how Effect sequences computations.
77
+
78
+ ### Bridge
79
+
80
+ You now have all the individual building blocks: `succeed`, `sync`, `fail`, `map`, `flatMap`, `andThen`, `tap`, `gen`. Kata 005 brings them together with **pipe composition** — building multi-step pipelines using both the standalone `pipe()` function and the fluent `.pipe()` method. It's the capstone of the Basics area.
@@ -0,0 +1,34 @@
1
+ import { Effect, Exit } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { lookupAndGreet, validateAndProcess, logAndReturn } from "@/katas/004-flatmap-and-chaining/solution.js";
4
+
5
+ describe("004 — FlatMap and Chaining", () => {
6
+ it("lookupAndGreet(0) succeeds with 'Hello, Alice!'", () => {
7
+ expect(Effect.runSync(lookupAndGreet(0))).toBe("Hello, Alice!");
8
+ });
9
+
10
+ it("lookupAndGreet(1) succeeds with 'Hello, Bob!'", () => {
11
+ expect(Effect.runSync(lookupAndGreet(1))).toBe("Hello, Bob!");
12
+ });
13
+
14
+ it("lookupAndGreet(99) fails with 'NotFound'", () => {
15
+ const exit = Effect.runSyncExit(lookupAndGreet(99));
16
+ expect(Exit.isFailure(exit)).toBe(true);
17
+ });
18
+
19
+ it("validateAndProcess('hello') succeeds with 'HELLO'", () => {
20
+ expect(Effect.runSync(validateAndProcess("hello"))).toBe("HELLO");
21
+ });
22
+
23
+ it("validateAndProcess('') fails with 'EmptyInput'", () => {
24
+ const exit = Effect.runSyncExit(validateAndProcess(""));
25
+ expect(Exit.isFailure(exit)).toBe(true);
26
+ });
27
+
28
+ it("logAndReturn records side effect and returns value", () => {
29
+ const log: string[] = [];
30
+ const result = Effect.runSync(logAndReturn("test", log));
31
+ expect(result).toBe("test");
32
+ expect(log).toContain("test");
33
+ });
34
+ });
@@ -0,0 +1,18 @@
1
+ import { Effect } from "effect";
2
+
3
+ /** Use Effect.flatMap to look up a name by id (0→"Alice", 1→"Bob", else fail with "NotFound")
4
+ * then return "Hello, {name}!" */
5
+ export const lookupAndGreet = (id: number): Effect.Effect<string, string> => {
6
+ throw new Error("Not implemented");
7
+ };
8
+
9
+ /** Use Effect.andThen to first validate input is non-empty (fail with "EmptyInput" if empty),
10
+ * then transform to uppercase */
11
+ export const validateAndProcess = (input: string): Effect.Effect<string, string> => {
12
+ throw new Error("Not implemented");
13
+ };
14
+
15
+ /** Use Effect.tap to record the value to a mutable array (sideEffects), then return the value unchanged */
16
+ export const logAndReturn = (value: string, sideEffects: string[]): Effect.Effect<string> => {
17
+ throw new Error("Not implemented");
18
+ };