| 1 | # Effect subtyping changes in v4 |
| 2 | |
| 3 | In v3, many types were structural subtypes of `Effect`. They carried the |
| 4 | Effect type ID at runtime and could be used anywhere an `Effect` was expected. |
| 5 | This included `Ref`, `Deferred`, `Fiber`, `FiberRef`, `Config`, `Option`, |
| 6 | `Either`, `Context.Tag`, and others. |
| 7 | |
| 8 | While convenient, this could cause subtle bugs. For example, passing a `Ref` |
| 9 | to an Effect combinator would read its value instead of treating the ref itself |
| 10 | as a value. |
| 11 | |
| 12 | In v4, several of these types are no longer Effect subtypes. Use explicit |
| 13 | conversion or module functions to obtain an Effect. There is no `Yieldable` |
| 14 | trait that makes non-Effect values usable in `Effect.gen`. |
| 15 | |
| 16 | ## Option and Result require conversion |
| 17 | |
| 18 | `Option` and `Result` (the replacement for v3's `Either`) are plain values, |
| 19 | not Effects. They do not have an `.asEffect()` method, and cannot be yielded |
| 20 | directly in `Effect.gen`. Their iterators work with `Option.gen` and |
| 21 | `Result.gen`, respectively, not with `Effect.gen`. |
| 22 | |
| 23 | Use `Effect.fromOption` or `Effect.fromResult` in Effect code: |
| 24 | |
| 25 | - `Effect.fromOption` succeeds with the contained value for `Some` and fails |
| 26 | with `Cause.NoSuchElementError` for `None`. An optional `onNone` callback |
| 27 | supplies a custom error. |
| 28 | - `Effect.fromResult` preserves the success value or failure error. |
| 29 | |
| 30 | **v3**: Option is an Effect subtype, so this compiles: |
| 31 | |
| 32 | ```ts |
| 33 | import { Effect, Option } from "effect" |
| 34 | |
| 35 | // Option<number> is assignable to Effect<number, NoSuchElementError> |
| 36 | const program = Effect.map(Option.some(42), (n) => n + 1) |
| 37 | ``` |
| 38 | |
| 39 | **v4**: Convert explicitly, both in combinators and in generators: |
| 40 | |
| 41 | ```ts |
| 42 | import { Effect, Option, Result } from "effect" |
| 43 | |
| 44 | // Effect<number, Cause.NoSuchElementError> |
| 45 | const program = Effect.map(Effect.fromOption(Option.some(42)), (n) => n + 1) |
| 46 | |
| 47 | // Effect<number, Cause.NoSuchElementError> |
| 48 | const program2 = Effect.gen(function*() { |
| 49 | const n = yield* Effect.fromOption(Option.some(42)) |
| 50 | return n + 1 |
| 51 | }) |
| 52 | |
| 53 | // Effect<number, never> |
| 54 | const program3 = Effect.gen(function*() { |
| 55 | const n = yield* Effect.fromResult(Result.succeed(123)) |
| 56 | return n + 1 |
| 57 | }) |
| 58 | |
| 59 | // Effect<never, string> |
| 60 | const failed = Effect.fromResult(Result.fail("failed")) |
| 61 | |
| 62 | // Effect<never, string> |
| 63 | const missing = Effect.fromOption(Option.none(), () => "missing") |
| 64 | ``` |
| 65 | |
| 66 | ## Config and services remain Effects |
| 67 | |
| 68 | `Config` and `Context.Service` remain Effect subtypes. You can yield them |
| 69 | in `Effect.gen` or pass them directly to Effect combinators without conversion. |
| 70 | |
| 71 | ```ts |
| 72 | import { Config, Context, Effect } from "effect" |
| 73 | |
| 74 | class Greeting extends Context.Service<Greeting, { readonly message: string }>()("Greeting") {} |
| 75 | |
| 76 | const program = Effect.gen(function*() { |
| 77 | const name = yield* Config.String("NAME").pipe(Config.withDefault("world")) |
| 78 | const greeting = yield* Greeting |
| 79 | return `${greeting.message}, ${name}!` |
| 80 | }).pipe(Effect.provideService(Greeting, { message: "Hello" })) |
| 81 | |
| 82 | const port = Effect.map(Config.Port("PORT").pipe(Config.withDefault(3000)), (n) => n + 1) |
| 83 | ``` |
| 84 | |
| 85 | `Effectable.Class.asEffect()` is a mechanism for defining custom Effects, |
| 86 | not a conversion method available on every iterable value. |
| 87 | |
| 88 | ## Types no longer subtypes of Effect |
| 89 | |
| 90 | Several types that extended `Effect` in v3 no longer do so in v4. Use the |
| 91 | appropriate module functions instead. |
| 92 | |
| 93 | **v3**: `Ref` extends `Effect<A>`, yielding the current value: |
| 94 | |
| 95 | ```ts |
| 96 | import { Effect, Ref } from "effect" |
| 97 | |
| 98 | const program = Effect.gen(function*() { |
| 99 | const ref = yield* Ref.make(0) |
| 100 | const value = yield* ref // Ref is an Effect<number> |
| 101 | }) |
| 102 | ``` |
| 103 | |
| 104 | **v4**: `Ref` is a plain value, use `Ref.get`: |
| 105 | |
| 106 | ```ts |
| 107 | import { Effect, Ref } from "effect" |
| 108 | |
| 109 | const program = Effect.gen(function*() { |
| 110 | const ref = yield* Ref.make(0) |
| 111 | const value = yield* Ref.get(ref) |
| 112 | return value |
| 113 | }) |
| 114 | ``` |
| 115 | |
| 116 | **v3**: `Deferred` extends `Effect<A, E>`, resolving when completed: |
| 117 | |
| 118 | ```ts |
| 119 | import { Deferred, Effect } from "effect" |
| 120 | |
| 121 | const program = Effect.gen(function*() { |
| 122 | const deferred = yield* Deferred.make<string, never>() |
| 123 | const value = yield* deferred // Deferred is an Effect<string> |
| 124 | }) |
| 125 | ``` |
| 126 | |
| 127 | **v4**: `Deferred` is a plain value, use `Deferred.await`: |
| 128 | |
| 129 | ```ts |
| 130 | import { Deferred, Effect } from "effect" |
| 131 | |
| 132 | const program = Effect.gen(function*() { |
| 133 | const deferred = yield* Deferred.make<string, never>() |
| 134 | yield* Deferred.succeed(deferred, "done") |
| 135 | const value = yield* Deferred.await(deferred) |
| 136 | return value |
| 137 | }) |
| 138 | ``` |
| 139 | |
| 140 | **v3**: `Fiber` extends `Effect<A, E>`, joining on yield: |
| 141 | |
| 142 | ```ts |
| 143 | import { Effect, Fiber } from "effect" |
| 144 | |
| 145 | const program = Effect.gen(function*() { |
| 146 | const fiber = yield* Effect.fork(task) |
| 147 | const result = yield* fiber // Fiber is an Effect<A, E> |
| 148 | }) |
| 149 | ``` |
| 150 | |
| 151 | **v4**: `Fiber` is a plain value, use `Fiber.join`: |
| 152 | |
| 153 | ```ts |
| 154 | import { Effect, Fiber } from "effect" |
| 155 | |
| 156 | const program = Effect.gen(function*() { |
| 157 | const task = Effect.succeed(42) |
| 158 | const fiber = yield* Effect.forkChild(task) |
| 159 | const result = yield* Fiber.join(fiber) |
| 160 | return result |
| 161 | }) |
| 162 | ``` |
| 163 | |
| 164 | ## Why this changed |
| 165 | |
| 166 | The v3 subtyping approach meant the type system could not distinguish between |
| 167 | "I have a Ref" and "I have an Effect that reads the Ref." For example, |
| 168 | `Effect.all` could accept an array of refs and silently read all of them. |
| 169 | |
| 170 | In v4, explicit functions such as `Ref.get`, `Deferred.await`, `Fiber.join`, |
| 171 | `Effect.fromOption`, and `Effect.fromResult` make these operations visible |
| 172 | and prevent those values from being passed accidentally to Effect combinators. |