@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
@@ -0,0 +1,38 @@
1
+ import { Data, Effect } from "effect";
2
+
3
+ export class NetworkError extends Data.TaggedError("NetworkError")<{
4
+ readonly url: string;
5
+ }> {}
6
+
7
+ export class TimeoutError extends Data.TaggedError("TimeoutError")<{
8
+ readonly ms: number;
9
+ }> {}
10
+
11
+ export class AuthError extends Data.TaggedError("AuthError")<{
12
+ readonly reason: string;
13
+ }> {}
14
+
15
+ /** Use Effect.catchTags to handle each error type differently:
16
+ * NetworkError → "network error: {url}"
17
+ * TimeoutError → "timeout after {ms}ms"
18
+ * AuthError → "auth failed: {reason}" */
19
+ export const handleAllErrors = (
20
+ effect: Effect.Effect<string, NetworkError | TimeoutError | AuthError>,
21
+ ): Effect.Effect<string> => {
22
+ throw new Error("Not implemented");
23
+ };
24
+
25
+ /** Use Effect.orElse to try the primary effect, and if it fails, run the fallback */
26
+ export const withFallback = (
27
+ primary: Effect.Effect<string, string>,
28
+ fallback: Effect.Effect<string, string>,
29
+ ): Effect.Effect<string, string> => {
30
+ throw new Error("Not implemented");
31
+ };
32
+
33
+ /** Use Effect.match to return "ok: {value}" on success or "err: {error}" on failure */
34
+ export const toResult = (
35
+ effect: Effect.Effect<string, string>,
36
+ ): Effect.Effect<string> => {
37
+ throw new Error("Not implemented");
38
+ };
@@ -0,0 +1,96 @@
1
+ # SENSEI — 009 Option Type
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Work with the Option type to safely represent values that may or may not exist.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `fromNullable` — convert a nullable value to an Option
12
+ 2. Implement `describe` — use `Option.match` to return "Found: {value}" or "Nothing"
13
+ 3. Implement `doubleOption` — use `Option.map` to double the number inside an Option
14
+ 4. Implement `safeDivide` — use `Option.flatMap` to divide safely (None if divisor is 0)
15
+ 5. Implement `getOrDefault` — use `Option.getOrElse` to extract value or return a default
16
+
17
+ ### Hints
18
+
19
+ ```ts
20
+ import { Option } from "effect";
21
+
22
+ // Create Options
23
+ const some = Option.some(42);
24
+ const none = Option.none();
25
+
26
+ // fromNullable
27
+ const opt = Option.fromNullable(null); // None
28
+ const opt2 = Option.fromNullable(42); // Some(42)
29
+
30
+ // match
31
+ const result = Option.match(opt, {
32
+ onNone: () => "nothing",
33
+ onSome: (v) => `found: ${v}`,
34
+ });
35
+
36
+ // map and flatMap
37
+ const doubled = Option.map(some, (n) => n * 2);
38
+ const chained = Option.flatMap(some, (n) =>
39
+ n > 0 ? Option.some(n) : Option.none()
40
+ );
41
+
42
+ // getOrElse
43
+ const value = Option.getOrElse(none, () => 0);
44
+ ```
45
+
46
+ ## Prerequisites
47
+
48
+ - **001-005 Basics** — `Effect.succeed`, `Effect.map`, `pipe`, `Effect.gen`, `Effect.flatMap`
49
+ - **006-008 Error Handling** — `Effect.fail`, `catchAll`, `catchTag`, `catchTags`, `match`
50
+
51
+ ## Skills
52
+
53
+ Invoke `effect-patterns-value-handling` before teaching this kata.
54
+
55
+ ## Test Map
56
+
57
+ | Test | Concept | Verifies |
58
+ |------|---------|----------|
59
+ | `fromNullable converts value to Some` | `Option.fromNullable` | Non-null value wrapped in Some |
60
+ | `fromNullable converts null to None` | `Option.fromNullable` | Null becomes None |
61
+ | `fromNullable converts undefined to None` | `Option.fromNullable` | Undefined becomes None |
62
+ | `describe returns 'Found: hello' for Some` | `Option.match` | Pattern matching on Some |
63
+ | `describe returns 'Nothing' for None` | `Option.match` | Pattern matching on None |
64
+ | `doubleOption doubles Some(5) to Some(10)` | `Option.map` | Transform inside Some |
65
+ | `doubleOption returns None for None` | `Option.map` | None passes through unchanged |
66
+ | `safeDivide(10, 2) returns Some(5)` | `Option.some` + `Option.flatMap` | Division succeeds — Some result |
67
+ | `safeDivide(10, 0) returns None` | `Option.none` | Division by zero — None result |
68
+ | `getOrDefault extracts Some value` | `Option.getOrElse` | Unwrap when value exists |
69
+ | `getOrDefault returns default for None` | `Option.getOrElse` | Fallback when value is absent |
70
+
71
+ ## Teaching Approach
72
+
73
+ ### Socratic prompts
74
+
75
+ - "What's the difference between `null` and `Option.none()`? Why bother wrapping it?"
76
+ - "You used `Effect.map` to transform Effects. `Option.map` works the same way. What does it do when the Option is None?"
77
+ - "How is `Option.flatMap` different from `Option.map`? When would your callback need to return an Option?"
78
+ - "In `safeDivide`, the result is `Option<number>`, not `Effect<number, DivisionByZero>`. When would you choose Option over Effect with an error?"
79
+
80
+ ### Common pitfalls
81
+
82
+ 1. **Confusing Option.flatMap with Effect.flatMap** — they work the same way but on different types. Option.flatMap takes `A => Option<B>`, Effect.flatMap takes `A => Effect<B, E, R>`. Ask: "Are you working with an Option or an Effect here?"
83
+ 2. **Using if/else instead of Option.match** — students may extract the value manually. Nudge: "Option.match handles both cases in one expression — what does it look like?"
84
+ 3. **fromNullable with falsy values** — `Option.fromNullable(0)` returns `Some(0)`, not `None`. Only `null` and `undefined` produce None. Ask: "Is `0` the same as `null`?"
85
+ 4. **Returning raw values from map** — `Option.map(opt, (n) => n * 2)` is correct. Students sometimes try to wrap the result in `Option.some` inside the callback. Ask: "What does `map` do with your callback's return value?"
86
+ 5. **Start with `fromNullable`** — just pass the value to `Option.fromNullable` and return the result; it's the simplest function here.
87
+
88
+ ## On Completion
89
+
90
+ ### Insight
91
+
92
+ Option is for "might not exist" — different from errors. Use Option when absence is a normal case (e.g., looking up a key in a map), not an error condition. Notice how Option has the same vocabulary as Effect: `map`, `flatMap`, `match`. This isn't a coincidence — Effect's design reuses these patterns across types so that once you learn them, they transfer everywhere.
93
+
94
+ ### Bridge
95
+
96
+ Option handles missing values. Kata 010 introduces `Either` for pure validation results (no effects needed) and `Exit` for inspecting effect outcomes after execution. `Effect.either` bridges the two worlds — converting an Effect's error channel into an Either value.
@@ -0,0 +1,49 @@
1
+ import { Option } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { fromNullable, describeOption, doubleOption, safeDivide, getOrDefault } from "@/katas/009-option-type/solution.js";
4
+
5
+ describe("009 — Option Type", () => {
6
+ it("fromNullable converts value to Some", () => {
7
+ expect(fromNullable(42)).toEqual(Option.some(42));
8
+ });
9
+
10
+ it("fromNullable converts null to None", () => {
11
+ expect(fromNullable(null)).toEqual(Option.none());
12
+ });
13
+
14
+ it("fromNullable converts undefined to None", () => {
15
+ expect(fromNullable(undefined)).toEqual(Option.none());
16
+ });
17
+
18
+ it("describe returns 'Found: hello' for Some", () => {
19
+ expect(describeOption(Option.some("hello"))).toBe("Found: hello");
20
+ });
21
+
22
+ it("describe returns 'Nothing' for None", () => {
23
+ expect(describeOption(Option.none())).toBe("Nothing");
24
+ });
25
+
26
+ it("doubleOption doubles Some(5) to Some(10)", () => {
27
+ expect(doubleOption(Option.some(5))).toEqual(Option.some(10));
28
+ });
29
+
30
+ it("doubleOption returns None for None", () => {
31
+ expect(doubleOption(Option.none())).toEqual(Option.none());
32
+ });
33
+
34
+ it("safeDivide(10, 2) returns Some(5)", () => {
35
+ expect(safeDivide(10, 2)).toEqual(Option.some(5));
36
+ });
37
+
38
+ it("safeDivide(10, 0) returns None", () => {
39
+ expect(safeDivide(10, 0)).toEqual(Option.none());
40
+ });
41
+
42
+ it("getOrDefault extracts Some value", () => {
43
+ expect(getOrDefault(Option.some(42), 0)).toBe(42);
44
+ });
45
+
46
+ it("getOrDefault returns default for None", () => {
47
+ expect(getOrDefault(Option.none(), 0)).toBe(0);
48
+ });
49
+ });
@@ -0,0 +1,26 @@
1
+ import { Option } from "effect";
2
+
3
+ /** Convert a nullable value to an Option */
4
+ export const fromNullable = <A>(value: A | null | undefined): Option.Option<A> => {
5
+ throw new Error("Not implemented");
6
+ };
7
+
8
+ /** Use Option.match to return "Found: {value}" for Some, "Nothing" for None */
9
+ export const describeOption = (opt: Option.Option<string>): string => {
10
+ throw new Error("Not implemented");
11
+ };
12
+
13
+ /** Use Option.map to double the number inside the Option */
14
+ export const doubleOption = (opt: Option.Option<number>): Option.Option<number> => {
15
+ throw new Error("Not implemented");
16
+ };
17
+
18
+ /** Use Option.flatMap to safely divide a by b (return None if b is 0) */
19
+ export const safeDivide = (a: number, b: number): Option.Option<number> => {
20
+ throw new Error("Not implemented");
21
+ };
22
+
23
+ /** Use Option.getOrElse to extract value or return a default */
24
+ export const getOrDefault = (opt: Option.Option<number>, defaultValue: number): number => {
25
+ throw new Error("Not implemented");
26
+ };
@@ -0,0 +1,86 @@
1
+ # SENSEI — 010 Either and Exit
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Use Either for pure validation and Exit to inspect effect results without throwing.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `safeRun` — use `Effect.either` and `Either.match` to return "ok: {value}" or "err: {error}"
12
+ 2. Implement `validatePositive` — return `Either.right(n)` if n > 0, `Either.left("not positive")` otherwise
13
+ 3. Implement `inspectExit` — use `Effect.runSyncExit` and `Exit.match` to return "success: {value}" or "failure"
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Effect, Either, Exit } from "effect";
19
+
20
+ // Effect.either converts error channel to Either
21
+ const safe = effect.pipe(
22
+ Effect.either,
23
+ Effect.map(
24
+ Either.match({
25
+ onLeft: (e) => `err: ${e}`,
26
+ onRight: (a) => `ok: ${a}`,
27
+ })
28
+ )
29
+ );
30
+
31
+ // Either for pure validation
32
+ const validated = n > 0 ? Either.right(n) : Either.left("invalid");
33
+
34
+ // Exit inspection
35
+ const exit = Effect.runSyncExit(effect);
36
+ const result = Exit.match(exit, {
37
+ onFailure: () => "failed",
38
+ onSuccess: (a) => `success: ${a}`,
39
+ });
40
+ ```
41
+
42
+ ## Prerequisites
43
+
44
+ - **009 Option Type** — `Option`, `some`, `none`, `match`, `map`, `flatMap`
45
+
46
+ > **Note**: Unlike previous katas, the user DOES write `Effect.runSyncExit` and `Exit.match` in `inspectExit`. This is intentional — inspecting an Exit is the lesson.
47
+
48
+ ## Test Map
49
+
50
+ | Test | Concept | Verifies |
51
+ |------|---------|----------|
52
+ | `safeRun wraps success` | `Effect.either` + `Either.match` | Success becomes Right, matched to string |
53
+ | `safeRun wraps failure` | `Effect.either` + `Either.match` | Failure becomes Left, matched to string |
54
+ | `validatePositive returns Right for positive` | `Either.right` | Pure validation — valid input |
55
+ | `validatePositive returns Left for zero` | `Either.left` | Pure validation — zero rejected |
56
+ | `validatePositive returns Left for negative` | `Either.left` | Pure validation — negative rejected |
57
+ | `inspectExit returns success string` | `Effect.runSyncExit` + `Exit.match` | Execute and inspect successful exit |
58
+ | `inspectExit returns failure string` | `Effect.runSyncExit` + `Exit.match` | Execute and inspect failed exit |
59
+
60
+ ## Teaching Approach
61
+
62
+ ### Socratic prompts
63
+
64
+ - "You've used `Effect.catchAll` to recover from errors. What if instead of recovering, you just want to see whether it succeeded or failed — without losing the information?"
65
+ - "`Effect.either` turns `Effect<A, E>` into `Effect<Either<A, E>, never>`. The error channel becomes `never`. Where did the error go?"
66
+ - "Either doesn't need an Effect to be useful. `validatePositive` is a pure function. When would you use Either without Effect?"
67
+ - "Exit looks a lot like Either. What's the difference? When do you encounter an Exit?"
68
+
69
+ ### Common pitfalls
70
+
71
+ 1. **Confusing Either and Effect** — Either is a pure data structure (Left or Right), not an Effect. `Either.right(5)` is a value, not a computation. Ask: "Does `Either.right(5)` need to be run?"
72
+ 2. **Either.match argument order** — `Either.match` takes `{ onLeft, onRight }`. Students may mix up which handler is for success vs failure. Ask: "In Either, which side is the success — Left or Right?"
73
+ 3. **Exit vs Either** — Exit is what you get after running an Effect. It's similar to Either but lives in the "execution" world, not the "pure data" world. Ask: "When do you get an Exit? Before or after running an Effect?"
74
+ 4. **Forgetting Effect.either in safeRun** — students may try to use try/catch or Effect.catchAll. Nudge: "`Effect.either` gives you the Either directly — no catching needed."
75
+ 5. **inspectExit uses runSyncExit** — this is the one place in the kata series where the user writes runtime execution code. It's intentional. Ask: "Why would you want to inspect the Exit rather than just running the Effect normally?"
76
+ 6. **Start with `validatePositive`** — it's pure, no Effects: if `n` is positive, return `Either.right(n)`, otherwise return `Either.left` with an error message.
77
+
78
+ ## On Completion
79
+
80
+ ### Insight
81
+
82
+ Either is for pure validation (no effects needed). Exit is for inspecting effect outcomes after execution. `Effect.either` bridges the two worlds — converting an Effect's error channel into an Either value. Notice the pattern: Option (might not exist), Either (might be wrong), Exit (might have failed at runtime). Each represents a different kind of "two outcomes" at a different level of abstraction.
83
+
84
+ ### Bridge
85
+
86
+ Value Handling is now complete. You can model missing values (Option), validation results (Either), and execution outcomes (Exit). Next up is **Dependency Injection** — kata 011 introduces `Context.Tag` and `provideService`, giving your Effects access to external services without hardcoding them.
@@ -0,0 +1,33 @@
1
+ import { Effect, Either } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { safeRun, validatePositive, inspectExit } from "@/katas/010-either-and-exit/solution.js";
4
+
5
+ describe("010 — Either and Exit", () => {
6
+ it("safeRun wraps success", () => {
7
+ expect(Effect.runSync(safeRun(Effect.succeed("hello")))).toBe("ok: hello");
8
+ });
9
+
10
+ it("safeRun wraps failure", () => {
11
+ expect(Effect.runSync(safeRun(Effect.fail("boom")))).toBe("err: boom");
12
+ });
13
+
14
+ it("validatePositive returns Right for positive", () => {
15
+ expect(validatePositive(5)).toEqual(Either.right(5));
16
+ });
17
+
18
+ it("validatePositive returns Left for zero", () => {
19
+ expect(Either.isLeft(validatePositive(0))).toBe(true);
20
+ });
21
+
22
+ it("validatePositive returns Left for negative", () => {
23
+ expect(Either.isLeft(validatePositive(-3))).toBe(true);
24
+ });
25
+
26
+ it("inspectExit returns success string", () => {
27
+ expect(inspectExit(Effect.succeed("data"))).toBe("success: data");
28
+ });
29
+
30
+ it("inspectExit returns failure string", () => {
31
+ expect(inspectExit(Effect.fail("oops"))).toBe("failure");
32
+ });
33
+ });
@@ -0,0 +1,17 @@
1
+ import { Effect, Either, Exit } from "effect";
2
+
3
+ /** Use Effect.either to convert an Effect<A, E> into Effect<Either<A, E>>
4
+ * then use Either.match to return "ok: {value}" or "err: {error}" */
5
+ export const safeRun = (effect: Effect.Effect<string, string>): Effect.Effect<string> => {
6
+ throw new Error("Not implemented");
7
+ };
8
+
9
+ /** Use Either.right and Either.left to validate: return Right(n) if n > 0, Left("not positive") otherwise */
10
+ export const validatePositive = (n: number): Either.Either<number, string> => {
11
+ throw new Error("Not implemented");
12
+ };
13
+
14
+ /** Use Effect.runSyncExit and Exit.match to return "success: {value}" or "failure" */
15
+ export const inspectExit = (effect: Effect.Effect<string, string>): string => {
16
+ throw new Error("Not implemented");
17
+ };
@@ -0,0 +1,82 @@
1
+ # SENSEI — 011 Services and Context
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn how Effect manages dependencies through the R (Requirements) channel.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `getRandomNumber` — access the `Random` service and return its `next` value
12
+ 2. Implement `rollDice` — access the `Random` service and return `"Roll: {n}"` where `n` is the random value
13
+
14
+ ### Hints
15
+
16
+ ```ts
17
+ import { Context, Effect } from "effect";
18
+
19
+ // Declare a service with Context.Tag
20
+ class MyService extends Context.Tag("MyService")<
21
+ MyService,
22
+ { readonly getValue: Effect.Effect<number> }
23
+ >() {}
24
+
25
+ // Access the service inside Effect.gen
26
+ const program = Effect.gen(function* () {
27
+ const svc = yield* MyService;
28
+ const value = yield* svc.getValue;
29
+ return value;
30
+ });
31
+
32
+ // Provide a concrete implementation
33
+ const result = Effect.runSync(
34
+ Effect.provideService(program, MyService, {
35
+ getValue: Effect.succeed(42),
36
+ }),
37
+ );
38
+ ```
39
+
40
+ ## Prerequisites
41
+
42
+ - **001-005 Basics** — `Effect.succeed`, `Effect.sync`, `Effect.map`, `pipe`, `Effect.gen`, `yield*`, `Effect.flatMap`
43
+ - **006-008 Error Handling** — `Effect.fail`, `catchAll`, `catchTag`, `Data.TaggedError`
44
+ - **009-010 Value Handling** — `Option`, `Either`, `Exit`
45
+
46
+ ## Skills
47
+
48
+ Invoke `effect-patterns-core-concepts` before teaching this kata. This is the first kata in the Dependency Injection area.
49
+
50
+ > **Note**: `Effect.runSync` and `Effect.provideService` appear only in tests. The student does NOT write them. Never attribute them to their learning.
51
+
52
+ ## Test Map
53
+
54
+ | Test | Concept | Verifies |
55
+ |------|---------|----------|
56
+ | `getRandomNumber returns the service value` | `yield* Random` + `yield* svc.next` | Accessing a service and calling its method |
57
+ | `rollDice formats the result` | `yield* Random` + `yield* svc.next` + string formatting | Service access plus value transformation |
58
+
59
+ ## Teaching Approach
60
+
61
+ ### Socratic prompts
62
+
63
+ - "You've used `yield*` to unwrap Effects. What happens if you `yield*` a Context.Tag like `Random`?"
64
+ - "After you get the service with `yield* Random`, you have an object with a `next` property. What is `next` — a value or an Effect? How do you get the value out?"
65
+ - "Look at the type of `getRandomNumber` — what does the `R` channel (the third type parameter) tell you?"
66
+
67
+ ### Common pitfalls
68
+
69
+ 1. **Calling Random directly** — `Random.next` doesn't exist. `Random` is a tag, not an instance. You need `yield* Random` first to get the service implementation, THEN access `.next` on it. Ask: "What does `Random` represent — the service itself, or a key to look it up?"
70
+ 2. **Forgetting the second yield*** — `yield* Random` gives you the service object, but `svc.next` is still an Effect. You need a second `yield*` to unwrap it. Ask: "What's the type of `svc.next`? Is it a number or an Effect?"
71
+ 3. **String formatting in rollDice** — students may return the number instead of the formatted string. The test expects `"Roll: 4"`. Nudge: "Check the test — what exact string format does `rollDice` need to produce?"
72
+ 4. **Two-step yield pattern** — inside `Effect.gen`, first `yield*` the `Random` tag to get the service, then `yield*` its `next` method to get the number. The Briefing hints show this pattern.
73
+
74
+ ## On Completion
75
+
76
+ ### Insight
77
+
78
+ `yield* Random` doesn't call anything — it asks Effect's runtime to provide the Random service. The `R` type parameter tracks what's needed. This is dependency injection at the type level: your program declares its dependencies in its type signature, and the runtime satisfies them. Notice how the tests provide a `TestRandom` with a predictable value — your code never knew or cared where the implementation came from.
79
+
80
+ ### Bridge
81
+
82
+ You've seen how to USE services, but the tests had to manually wire them with `provideService`. Kata 012 introduces **Layers** — reusable recipes for building services. Layers decouple "how a service is built" from "how it's used", making large programs composable.
@@ -0,0 +1,23 @@
1
+ import { Effect } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { Random, getRandomNumber, rollDice } from "@/katas/011-services-and-context/solution.js";
4
+
5
+ const TestRandom = {
6
+ next: Effect.succeed(4),
7
+ };
8
+
9
+ describe("011 — Services and Context", () => {
10
+ it("getRandomNumber returns the service value", () => {
11
+ const result = Effect.runSync(
12
+ Effect.provideService(getRandomNumber, Random, TestRandom),
13
+ );
14
+ expect(result).toBe(4);
15
+ });
16
+
17
+ it("rollDice formats the result", () => {
18
+ const result = Effect.runSync(
19
+ Effect.provideService(rollDice, Random, TestRandom),
20
+ );
21
+ expect(result).toBe("Roll: 4");
22
+ });
23
+ });
@@ -0,0 +1,17 @@
1
+ import { Context, Effect } from "effect";
2
+
3
+ // A simple "Random" service that produces random numbers
4
+ export class Random extends Context.Tag("Random")<
5
+ Random,
6
+ { readonly next: Effect.Effect<number> }
7
+ >() {}
8
+
9
+ /** Use Effect.flatMap or Effect.gen to access the Random service and return its next value */
10
+ export const getRandomNumber = Effect.gen(function* () {
11
+ throw new Error("Not implemented");
12
+ });
13
+
14
+ /** Access Random service and return "Roll: {n}" where n is the random value */
15
+ export const rollDice = Effect.gen(function* () {
16
+ throw new Error("Not implemented");
17
+ });
@@ -0,0 +1,73 @@
1
+ # SENSEI — 012 Layers
2
+
3
+ ## Briefing
4
+
5
+ ### Goal
6
+
7
+ Learn how to compose service implementations using Layers.
8
+
9
+ ### Tasks
10
+
11
+ 1. Implement `ConfigLive` — a Layer that provides `Config` with `baseUrl` set to `"https://api.example.com"`
12
+ 2. Implement `LoggerLive` — a Layer that provides `Logger` with a `log` function that uses `Effect.log`
13
+ 3. Implement `getEndpoint` — read `Config.baseUrl` and `Logger.log` to return `"{baseUrl}/users"`
14
+
15
+ ### Hints
16
+
17
+ ```ts
18
+ import { Context, Effect, Layer } from "effect";
19
+
20
+ // Create a Layer from a value
21
+ const MyLayer = Layer.succeed(MyService, { value: 42 });
22
+
23
+ // Create a Layer from an effect
24
+ const MyEffectLayer = Layer.effect(
25
+ MyService,
26
+ Effect.succeed({ value: 42 }),
27
+ );
28
+
29
+ // Merge layers
30
+ const Combined = Layer.merge(LayerA, LayerB);
31
+
32
+ // Provide a layer to a program
33
+ const result = Effect.runSync(Effect.provide(program, Combined));
34
+ ```
35
+
36
+ ## Prerequisites
37
+
38
+ - **011 Services and Context** — `Context.Tag`, `yield*` on a tag, `yield*` on a service method
39
+
40
+ > **Note**: `Effect.runSync`, `Effect.provide`, `Layer.merge`, and `Layer.succeed` (in tests) appear only in tests. Never attribute them to the user's learning.
41
+
42
+ ## Test Map
43
+
44
+ | Test | Concept | Verifies |
45
+ |------|---------|----------|
46
+ | `ConfigLive provides baseUrl` | `Layer.succeed` | Creating a Layer with a static service value |
47
+ | `getEndpoint returns full URL` | `yield* Config` + `yield* Logger` + format | Multi-service program using two services |
48
+
49
+ ## Teaching Approach
50
+
51
+ ### Socratic prompts
52
+
53
+ - "In kata 011, the tests used `provideService` to wire one service at a time. What if you have TWO services? How would you provide both?"
54
+ - "What's the difference between `Layer.succeed(Config, { baseUrl: '...' })` and directly providing the service? When would the Layer approach be better?"
55
+ - "For `getEndpoint`, you need both Config and Logger. Does the order you `yield*` them matter?"
56
+
57
+ ### Common pitfalls
58
+
59
+ 1. **Confusing `Layer.succeed` and `Layer.effect`** — `Layer.succeed(Tag, value)` is for static values. `Layer.effect(Tag, someEffect)` is when building the service requires running an Effect. For `ConfigLive`, a static config object works. Ask: "Is the config value known upfront, or does it need computation?"
60
+ 2. **Forgetting to call Logger.log** — `getEndpoint` needs to use the Logger service. Students may skip the logging step. Check the test to see if logging is required for the test to pass.
61
+ 3. **Layer type annotation** — the stub uses a type cast. Students need to replace the entire implementation with a proper `Layer.succeed(Config, { ... })` call. Nudge: "Delete the placeholder and write a fresh `Layer.succeed` call."
62
+ 4. **String concatenation in getEndpoint** — the test expects `"https://test.com/users"`. Make sure to include the `/` between baseUrl and `"users"`.
63
+ 5. **Start with `Layer.succeed`** — the simplest form is `Layer.succeed(Config, { baseUrl: 'https://api.example.com' })`. Use that pattern for both `ConfigLive` and `LoggerLive`.
64
+
65
+ ## On Completion
66
+
67
+ ### Insight
68
+
69
+ Layers decouple "how a service is built" from "how it's used". The program says WHAT it needs (via the R channel), the Layer says HOW to provide it. This separation is what makes Effect programs composable and testable. Notice how the test swapped in a completely different Config and a no-op Logger — your `getEndpoint` code didn't change at all.
70
+
71
+ ### Bridge
72
+
73
+ You've built services and wired them with Layers. But the real power shows up in testing. Kata 013 makes this explicit: you'll write programs against a service interface, and the tests will provide different implementations — no mocking frameworks needed.
@@ -0,0 +1,23 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { describe, expect, it } from "vitest";
3
+ import { Config, Logger, ConfigLive, LoggerLive, getEndpoint } from "@/katas/012-layers/solution.js";
4
+
5
+ describe("012 — Layers", () => {
6
+ it("ConfigLive provides baseUrl", () => {
7
+ const program = Effect.gen(function* () {
8
+ const config = yield* Config;
9
+ return config.baseUrl;
10
+ });
11
+ const result = Effect.runSync(Effect.provide(program, ConfigLive));
12
+ expect(result).toBe("https://api.example.com");
13
+ });
14
+
15
+ it("getEndpoint returns full URL", () => {
16
+ const TestLayer = Layer.merge(
17
+ Layer.succeed(Config, { baseUrl: "https://test.com" }),
18
+ Layer.succeed(Logger, { log: () => Effect.void }),
19
+ );
20
+ const result = Effect.runSync(Effect.provide(getEndpoint, TestLayer));
21
+ expect(result).toBe("https://test.com/users");
22
+ });
23
+ });
@@ -0,0 +1,26 @@
1
+ import { Context, Effect, Layer } from "effect";
2
+
3
+ export class Config extends Context.Tag("Config")<
4
+ Config,
5
+ { readonly baseUrl: string }
6
+ >() {}
7
+
8
+ export class Logger extends Context.Tag("Logger")<
9
+ Logger,
10
+ { readonly log: (msg: string) => Effect.Effect<void> }
11
+ >() {}
12
+
13
+ /** Create a Layer that provides Config with baseUrl "https://api.example.com" */
14
+ export const ConfigLive: Layer.Layer<Config> = Layer.effectDiscard(
15
+ Effect.die("Not implemented"),
16
+ ) as unknown as Layer.Layer<Config>;
17
+
18
+ /** Create a Layer that provides Logger with a log function that uses Effect.log */
19
+ export const LoggerLive: Layer.Layer<Logger> = Layer.effectDiscard(
20
+ Effect.die("Not implemented"),
21
+ ) as unknown as Layer.Layer<Logger>;
22
+
23
+ /** Create a program that reads Config.baseUrl and Logger.log to return "{baseUrl}/users" */
24
+ export const getEndpoint = Effect.gen(function* () {
25
+ throw new Error("Not implemented");
26
+ });