@zio.dev/zio-blocks 0.0.1 → 0.0.22
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/index.md +163 -15
- package/package.json +5 -2
- package/path-interpolator.md +24 -23
- package/reference/binding.md +8 -14
- package/reference/chunk.md +36 -36
- package/reference/codec.md +384 -0
- package/reference/docs.md +2 -2
- package/reference/dynamic-optic.md +392 -0
- package/reference/dynamic-value.md +34 -34
- package/reference/formats.md +84 -28
- package/reference/json-schema.md +33 -30
- package/reference/json.md +47 -47
- package/reference/lazy.md +361 -0
- package/reference/modifier.md +151 -87
- package/reference/optics.md +51 -37
- package/reference/reflect.md +3 -3
- package/reference/registers.md +7 -7
- package/reference/schema.md +18 -18
- package/reference/syntax.md +27 -27
- package/reference/type-class-derivation.md +1959 -0
- package/scope.md +521 -203
- package/sidebars.js +6 -1
- package/reference/reflect-transform.md +0 -387
- package/reference/type-class-derivation-internals.md +0 -632
package/scope.md
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
`zio.blocks.scope` provides **compile-time verified resource safety** for synchronous code by tagging values with an unnameable, type-level **scope identity**. Values allocated in a scope can only be used when you hold a compatible `Scope`, and values allocated in a *child* scope cannot be returned to the parent in a usable form.
|
|
4
4
|
|
|
5
|
+
**Structured scopes.** Scopes follow the structured-concurrency philosophy: child scopes are nested within parent scopes, resources are tied to the lifetime of the scope that allocated them, and cleanup happens deterministically when the scope exits (finalizers run LIFO). This "nesting = lifetime" structure provides clear ownership boundaries in addition to compile-time leak prevention.
|
|
6
|
+
|
|
5
7
|
If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the same problem space, but it focuses on:
|
|
6
8
|
|
|
7
9
|
- **Compile-time prevention of scope leaks**
|
|
8
|
-
- **Zero
|
|
10
|
+
- **Zero-cost opaque type** (`$[A]` is the scoped type, equal to `A` at runtime)
|
|
9
11
|
- **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
|
|
12
|
+
- **Eager evaluation** (all operations execute immediately, no deferred thunks)
|
|
10
13
|
|
|
11
14
|
---
|
|
12
15
|
|
|
@@ -14,23 +17,24 @@ If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the
|
|
|
14
17
|
|
|
15
18
|
- [Quick start](#quick-start)
|
|
16
19
|
- [Core concepts](#core-concepts)
|
|
17
|
-
- [1) `Scope
|
|
18
|
-
- [2) Scoped values:
|
|
20
|
+
- [1) `Scope`](#1-scope)
|
|
21
|
+
- [2) Scoped values: `$[+A]`](#2-scoped-values-a)
|
|
19
22
|
- [3) `Resource[A]`: acquisition + finalization](#3-resourcea-acquisition--finalization)
|
|
20
|
-
- [4) `
|
|
21
|
-
- [5) `
|
|
23
|
+
- [4) `Unscoped`: marking pure data types](#4-unscoped-marking-pure-data-types)
|
|
24
|
+
- [5) `lower`: accessing parent-scoped values](#5-lower-accessing-parent-scoped-values)
|
|
22
25
|
- [6) `Wire[-In, +Out]`: dependency recipes](#6-wire-in-out-dependency-recipes)
|
|
23
|
-
- [7) `Wireable[Out]`: DI for traits/abstract classes](#7-wireableout-di-for-traitsabstract-classes)
|
|
24
26
|
- [Safety model (why leaking is prevented)](#safety-model-why-leaking-is-prevented)
|
|
25
27
|
- [Usage examples](#usage-examples)
|
|
26
28
|
- [Allocating and using a resource](#allocating-and-using-a-resource)
|
|
27
29
|
- [Nested scopes (child can use parent, not vice versa)](#nested-scopes-child-can-use-parent-not-vice-versa)
|
|
28
|
-
- [
|
|
30
|
+
- [Chaining resource acquisition](#chaining-resource-acquisition)
|
|
29
31
|
- [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
|
|
32
|
+
- [Classes with `Finalizer` parameters](#classes-with-finalizer-parameters)
|
|
30
33
|
- [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
34
|
+
- [Dependency injection with `Resource.from[T](wires*)`](#dependency-injection-with-resourcefromtwires)
|
|
35
|
+
- [Injecting traits via subtype wires](#injecting-traits-via-subtype-wires)
|
|
33
36
|
- [Interop escape hatch: `leak`](#interop-escape-hatch-leak)
|
|
37
|
+
- [Common compile errors](#common-compile-errors)
|
|
34
38
|
- [API reference (selected)](#api-reference-selected)
|
|
35
39
|
|
|
36
40
|
---
|
|
@@ -46,80 +50,113 @@ final class Database extends AutoCloseable {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
Scope.global.scoped { scope =>
|
|
49
|
-
|
|
50
|
-
scope.allocate(Resource(new Database))
|
|
53
|
+
import scope._
|
|
51
54
|
|
|
52
|
-
val
|
|
53
|
-
scope.$(db)(_.query("SELECT 1"))
|
|
55
|
+
val db: $[Database] = allocate(Resource(new Database))
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
// scope.use applies a function to the scoped value, returning a scoped result
|
|
58
|
+
val result: $[String] = scope.use(db)(_.query("SELECT 1"))
|
|
59
|
+
println(result) // $[String] = String at runtime, prints directly
|
|
56
60
|
}
|
|
57
61
|
```
|
|
58
62
|
|
|
59
63
|
Key things to notice:
|
|
60
64
|
|
|
61
|
-
- `
|
|
62
|
-
-
|
|
63
|
-
-
|
|
65
|
+
- `allocate(...)` returns a **scoped** value of type `$[Database]` (the path-dependent type of the enclosing scope)
|
|
66
|
+
- `$[A] = A` at runtime — zero-cost opaque type, no boxing
|
|
67
|
+
- All operations are **eager** — values are computed immediately, no lazy thunks
|
|
68
|
+
- Use `scope.use(value)(f)` to work with scoped values; returns `$[B]`
|
|
64
69
|
- When the `scoped { ... }` block exits, finalizers run **LIFO** and errors are handled safely
|
|
70
|
+
- The `scoped` method requires `Unscoped[A]` evidence on the return type
|
|
65
71
|
|
|
66
72
|
---
|
|
67
73
|
|
|
68
74
|
## Core concepts
|
|
69
75
|
|
|
70
|
-
### 1) `Scope
|
|
76
|
+
### 1) `Scope`
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
`Scope` is a `sealed abstract class` with **no** type parameters. It manages finalizers and ties values to a *type-level identity* via abstract type members.
|
|
73
79
|
|
|
74
|
-
-
|
|
75
|
-
|
|
76
|
-
|
|
80
|
+
- **`type $[+A]`** — a path-dependent opaque type that tags values to this scope. Covariant in `A`. Equal to `A` at runtime (zero-cost).
|
|
81
|
+
- **`type Parent <: Scope`** — the parent scope's type.
|
|
82
|
+
- **`val parent: Parent`** — reference to the parent scope.
|
|
77
83
|
|
|
78
|
-
|
|
84
|
+
Each scope instance exposes its own `$[+A]`, so a parent's `$[Database]` is a different type than a child's `$[Database]`, even though both equal `Database` at runtime.
|
|
79
85
|
|
|
80
86
|
```scala
|
|
81
|
-
type
|
|
87
|
+
type $[+A] // = A at runtime (zero-cost)
|
|
82
88
|
```
|
|
83
89
|
|
|
84
90
|
So in code you'll typically write:
|
|
85
91
|
|
|
86
92
|
```scala
|
|
87
93
|
Scope.global.scoped { scope =>
|
|
88
|
-
|
|
94
|
+
import scope._
|
|
95
|
+
val x: $[Something] = ??? // or scope.$[Something]
|
|
89
96
|
}
|
|
90
97
|
```
|
|
91
98
|
|
|
99
|
+
Child scopes are represented by `Scope.Child[P <: Scope]`, a `final class` nested in the `Scope` companion object.
|
|
100
|
+
|
|
92
101
|
#### Global scope
|
|
93
102
|
|
|
94
|
-
`Scope.global` is the root of the
|
|
103
|
+
`Scope.global` is the root of the scope hierarchy:
|
|
95
104
|
|
|
96
105
|
```scala
|
|
97
106
|
object Scope {
|
|
98
|
-
|
|
99
|
-
|
|
107
|
+
object global extends Scope {
|
|
108
|
+
type $[+A] = A
|
|
109
|
+
type Parent = global.type
|
|
110
|
+
val parent: Parent = this
|
|
111
|
+
}
|
|
100
112
|
}
|
|
101
113
|
```
|
|
102
114
|
|
|
103
115
|
- The global scope is intended to live for the lifetime of the process.
|
|
104
116
|
- Its finalizers run on JVM shutdown.
|
|
105
|
-
- Values allocated in `Scope.global` typically **escape** as raw values via `ScopeEscape` (see below).
|
|
106
117
|
|
|
107
118
|
---
|
|
108
119
|
|
|
109
|
-
### 2) Scoped values:
|
|
120
|
+
### 2) Scoped values: `$[+A]`
|
|
110
121
|
|
|
111
|
-
`
|
|
122
|
+
`$[+A]` (or `scope.$[A]` in type annotations) is a path-dependent opaque type representing a value of type `A` that is locked to a specific scope. It is covariant in `A`.
|
|
112
123
|
|
|
113
|
-
- **Runtime representation:**
|
|
114
|
-
- **Key effect:** methods on `A` are hidden
|
|
124
|
+
- **Runtime representation:** `$[A] = A` — zero-cost opaque type, no boxing or wrapping
|
|
125
|
+
- **Key effect:** methods on `A` are hidden at the type level; you can't call `a.method` directly
|
|
126
|
+
- **All operations are eager:** `allocate(resource)` acquires the resource **immediately** and returns a scoped value
|
|
115
127
|
- **Access paths:**
|
|
116
|
-
- `scope
|
|
117
|
-
|
|
128
|
+
- `scope.use(a)(f)` to apply a function and get `$[B]`
|
|
129
|
+
|
|
130
|
+
#### `ScopedOps`: `map` and `flatMap` on `$[A]`
|
|
131
|
+
|
|
132
|
+
`Scope` provides an implicit class `ScopedOps[A]` that adds `map` and `flatMap` to `$[A]` values, enabling for-comprehension syntax:
|
|
133
|
+
|
|
134
|
+
```scala
|
|
135
|
+
Scope.global.scoped { scope =>
|
|
136
|
+
import scope._
|
|
137
|
+
|
|
138
|
+
val x: $[Int] = $(42)
|
|
139
|
+
val y: $[String] = x.map(_.toString)
|
|
140
|
+
val z: $[String] = x.flatMap(v => $(s"value: $v"))
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- `sa.map(f: A => B): $[B]` — applies `f` to the unwrapped value, re-wraps the result
|
|
145
|
+
- `sa.flatMap(f: A => $[B]): $[B]` — applies `f` to the unwrapped value (where `f` returns a scoped value)
|
|
146
|
+
- All operations are **eager** (zero-cost)
|
|
147
|
+
|
|
148
|
+
#### Scala 2 note
|
|
149
|
+
|
|
150
|
+
In Scala 2, the `scoped` method must be called with a lambda literal. Passing a variable or method reference is not supported due to macro limitations:
|
|
118
151
|
|
|
119
|
-
|
|
152
|
+
```scala
|
|
153
|
+
// ✅ OK: lambda literal
|
|
154
|
+
Scope.global.scoped { scope => ... }
|
|
120
155
|
|
|
121
|
-
|
|
122
|
-
|
|
156
|
+
// ❌ ERROR in Scala 2 (works in Scala 3):
|
|
157
|
+
val f: Scope.Child[_] => Any = scope => ...
|
|
158
|
+
Scope.global.scoped(f)
|
|
159
|
+
```
|
|
123
160
|
|
|
124
161
|
---
|
|
125
162
|
|
|
@@ -128,7 +165,7 @@ object Scope {
|
|
|
128
165
|
`Resource[A]` describes how to **acquire** an `A` and how to **release** it when a scope closes. It is intentionally lazy: you *describe what to do*, and allocation happens only through:
|
|
129
166
|
|
|
130
167
|
```scala
|
|
131
|
-
|
|
168
|
+
allocate(resource)
|
|
132
169
|
```
|
|
133
170
|
|
|
134
171
|
Common constructors:
|
|
@@ -139,12 +176,12 @@ Common constructors:
|
|
|
139
176
|
- Explicit lifecycle.
|
|
140
177
|
- `Resource.fromAutoCloseable(thunk)`
|
|
141
178
|
- A type-safe helper for `AutoCloseable`.
|
|
142
|
-
- `Resource.from[T]` (macro)
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
179
|
+
- `Resource.from[T](wires*)` (macro)
|
|
180
|
+
- The primary entry point for dependency injection.
|
|
181
|
+
- Resolves `T` and all its dependencies into a single `Resource[T]`.
|
|
182
|
+
- Auto-creates missing wires using `Wire.shared` for concrete classes.
|
|
183
|
+
- Requires explicit wires for: primitives, functions, collections, and abstract types.
|
|
184
|
+
- If `T` or any dependency is `AutoCloseable`, registers `close()` automatically.
|
|
148
185
|
|
|
149
186
|
#### Resource "sharing" vs "uniqueness"
|
|
150
187
|
|
|
@@ -162,93 +199,138 @@ Common constructors:
|
|
|
162
199
|
|
|
163
200
|
---
|
|
164
201
|
|
|
165
|
-
### 4) `
|
|
202
|
+
### 4) `Unscoped`: marking pure data types
|
|
166
203
|
|
|
167
|
-
`
|
|
204
|
+
The `Unscoped[A]` typeclass marks types as pure data that don't hold resources. The `scoped` method requires `Unscoped[A]` evidence on the return type to ensure only safe values can exit a scope.
|
|
168
205
|
|
|
169
|
-
|
|
206
|
+
**Built-in Unscoped types:**
|
|
207
|
+
- Primitives: `Int`, `Long`, `Boolean`, `Double`, etc.
|
|
208
|
+
- `String`, `Unit`, `Nothing`
|
|
209
|
+
- Collections of Unscoped types
|
|
170
210
|
|
|
211
|
+
**Custom Unscoped types:**
|
|
171
212
|
```scala
|
|
172
|
-
|
|
213
|
+
// Scala 3:
|
|
214
|
+
case class Config(debug: Boolean)
|
|
215
|
+
object Config {
|
|
216
|
+
given Unscoped[Config] = Unscoped.derived
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Scala 2:
|
|
220
|
+
case class Config(debug: Boolean)
|
|
221
|
+
object Config {
|
|
222
|
+
implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
|
|
223
|
+
}
|
|
173
224
|
```
|
|
174
225
|
|
|
175
|
-
|
|
226
|
+
**Allowed return types from `scoped`:**
|
|
176
227
|
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
- `flatMap` composes scoped values while tracking combined requirements
|
|
180
|
-
- Or directly:
|
|
181
|
-
- `Scoped.create(() => ...)` (advanced/internal style)
|
|
228
|
+
- **`Unscoped` types**: Pure data that can safely exit
|
|
229
|
+
- **`Nothing`**: For blocks that throw
|
|
182
230
|
|
|
183
|
-
|
|
231
|
+
**Rejected return types (no Unscoped instance):**
|
|
232
|
+
|
|
233
|
+
- **Closures**: `() => A` could capture the child scope
|
|
234
|
+
- **Scoped values**: `$[A]` would be use-after-close
|
|
235
|
+
- **The scope itself**: Would allow operations after close
|
|
236
|
+
|
|
237
|
+
```scala
|
|
238
|
+
Scope.global.scoped { parent =>
|
|
239
|
+
import parent._
|
|
240
|
+
|
|
241
|
+
// ✅ OK: String is Unscoped
|
|
242
|
+
val result: String = scoped { child =>
|
|
243
|
+
"hello"
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ❌ COMPILE ERROR: $[Database] has no Unscoped instance
|
|
247
|
+
// val escaped = scoped { child =>
|
|
248
|
+
// import child._
|
|
249
|
+
// allocate(Resource(new Database)) // $[Database] can't escape
|
|
250
|
+
// }
|
|
251
|
+
}
|
|
252
|
+
```
|
|
184
253
|
|
|
185
254
|
---
|
|
186
255
|
|
|
187
|
-
### 5) `
|
|
256
|
+
### 5) `lower`: accessing parent-scoped values
|
|
257
|
+
|
|
258
|
+
When working in a child scope, you may need to access values allocated in a parent scope. Use `lower(parentValue)` to "lower" a parent-scoped value into the child scope:
|
|
188
259
|
|
|
189
|
-
|
|
260
|
+
```scala
|
|
261
|
+
Scope.global.scoped { parent =>
|
|
262
|
+
import parent._
|
|
190
263
|
|
|
191
|
-
|
|
192
|
-
- `scope(scopedComputation)`,
|
|
264
|
+
val parentDb: $[Database] = allocate(Resource(new Database))
|
|
193
265
|
|
|
194
|
-
|
|
266
|
+
scoped { child =>
|
|
267
|
+
import child._
|
|
195
268
|
|
|
196
|
-
-
|
|
197
|
-
|
|
269
|
+
// Use lower() to access parent-scoped value in child scope
|
|
270
|
+
val db: $[Database] = lower(parentDb)
|
|
271
|
+
child.use(db)(_.query("SELECT 1"))
|
|
198
272
|
|
|
199
|
-
|
|
273
|
+
"done"
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
200
277
|
|
|
201
|
-
|
|
202
|
-
- Resource-like values should remain scoped unless you explicitly `leak`.
|
|
278
|
+
The `lower` operation is necessary because each scope has its own `$[A]` opaque type. A parent's `$[A]` is a different type than a child's `$[A]`, even though both equal `A` at runtime.
|
|
203
279
|
|
|
204
280
|
---
|
|
205
281
|
|
|
206
282
|
### 6) `Wire[-In, +Out]`: dependency recipes
|
|
207
283
|
|
|
208
|
-
`Wire` is a recipe for constructing services,
|
|
284
|
+
`Wire` is a recipe for constructing services. It describes **how** to build a service given its dependencies, but does not resolve those dependencies itself.
|
|
209
285
|
|
|
210
286
|
- `In` is the required dependencies (provided as a `Context[In]`)
|
|
211
287
|
- `Out` is the produced service
|
|
212
288
|
|
|
213
289
|
There are two wire flavors:
|
|
214
290
|
|
|
215
|
-
- `Wire.Shared`: a shared
|
|
216
|
-
- `Wire.Unique`: a
|
|
291
|
+
- `Wire.Shared`: produces a shared (memoized) instance
|
|
292
|
+
- `Wire.Unique`: produces a fresh instance each time
|
|
217
293
|
|
|
218
|
-
**Important clarification:** `Wire` itself is just a recipe. The
|
|
294
|
+
**Important clarification:** `Wire` itself is just a recipe. The sharing/uniqueness behavior is realized when the wire is used inside `Resource.from`, which composes `Resource.Shared` or `Resource.Unique` instances accordingly.
|
|
219
295
|
|
|
220
|
-
|
|
221
|
-
val r: Resource[Out] = wire.toResource(deps)
|
|
222
|
-
val out: Out @@ scope.Tag = scope.allocate(r)
|
|
223
|
-
```
|
|
296
|
+
#### Creating wires
|
|
224
297
|
|
|
225
|
-
|
|
226
|
-
- `Wire.Unique#toResource` produces a `Resource.Unique`.
|
|
298
|
+
There are exactly **3 macro entry points**:
|
|
227
299
|
|
|
228
|
-
|
|
300
|
+
| Macro | Purpose |
|
|
301
|
+
|-------|---------|
|
|
302
|
+
| `Wire.shared[T]` | Create a shared wire from `T`'s constructor |
|
|
303
|
+
| `Wire.unique[T]` | Create a unique wire from `T`'s constructor |
|
|
304
|
+
| `Resource.from[T](wires*)` | Wire up `T` and all dependencies into a `Resource` |
|
|
229
305
|
|
|
230
|
-
|
|
231
|
-
- `unique[T]`: derive a unique wire
|
|
306
|
+
For wrapping pre-existing values:
|
|
232
307
|
|
|
233
|
-
|
|
308
|
+
- `Wire(value)` — wraps a value; if `AutoCloseable`, registers `close()` automatically
|
|
234
309
|
|
|
235
|
-
|
|
310
|
+
#### How `Resource.from[T](wires*)` works
|
|
236
311
|
|
|
237
|
-
|
|
312
|
+
1. **Collect wires**: Uses explicit wires when provided, otherwise auto-creates with `Wire.shared`
|
|
313
|
+
2. **Validate**: Checks for cycles, unmakeable types, duplicate providers
|
|
314
|
+
3. **Topological sort**: Orders dependencies so leaves are allocated first
|
|
315
|
+
4. **Generate composition**: Produces a `Resource[T]` via flatMap chains
|
|
238
316
|
|
|
239
|
-
|
|
317
|
+
Key insight: **Compose Resources, don't accumulate values.** Each wire becomes a `Resource`, and they are composed via `flatMap`. This correctly preserves:
|
|
240
318
|
|
|
241
|
-
|
|
319
|
+
- **Sharing**: Same `Resource.Shared` instance → same value (even in diamond patterns)
|
|
320
|
+
- **Uniqueness**: `Resource.Unique` → fresh value per injection site
|
|
242
321
|
|
|
243
|
-
|
|
244
|
-
- `shared[MyTrait]` or `unique[MyTrait]` will pick it up automatically.
|
|
322
|
+
#### Subtype resolution
|
|
245
323
|
|
|
246
|
-
|
|
324
|
+
When `Resource.from` needs a dependency of type `Service`, it will accept a wire whose output is a subtype (e.g., `Wire.shared[LiveService]` where `LiveService extends Service`). This enables trait injection without extra boilerplate.
|
|
325
|
+
|
|
326
|
+
If the same concrete wire satisfies multiple types (e.g., `Service` and `LiveService`), only **one instance** is created and reused for both.
|
|
247
327
|
|
|
248
328
|
---
|
|
249
329
|
|
|
250
330
|
## Safety model (why leaking is prevented)
|
|
251
331
|
|
|
332
|
+
**Pragmatic safety.** The type-level tagging prevents *accidental* scope misuse in normal code, but it is not a security boundary. A determined developer can bypass it via `leak` (which emits a compiler warning), unsafe casts (`asInstanceOf`), or storing scoped references in mutable state (`var`). The guarantees are "good enough" to catch mistakes in regular usage, not protection against intentional circumvention.
|
|
333
|
+
|
|
252
334
|
The library prevents scope leaks via two reinforcing mechanisms:
|
|
253
335
|
|
|
254
336
|
### A) Existential child tags (fresh, unnameable types)
|
|
@@ -257,20 +339,24 @@ Child scopes are created with:
|
|
|
257
339
|
|
|
258
340
|
```scala
|
|
259
341
|
Scope.global.scoped { scope =>
|
|
260
|
-
scope.
|
|
342
|
+
import scope._
|
|
343
|
+
scoped { child =>
|
|
344
|
+
import child._
|
|
261
345
|
// allocate in child
|
|
262
346
|
}
|
|
263
347
|
}
|
|
264
348
|
```
|
|
265
349
|
|
|
266
|
-
The child scope has
|
|
350
|
+
The child scope has a fresh, unnameable `$[A]` type (created per invocation). You can allocate in the child, but you can't return those values to the parent in a usable form because the parent cannot name (or satisfy) the child's `$[A]` type.
|
|
267
351
|
|
|
268
352
|
Compile-time safety is verified in tests, e.g.:
|
|
269
353
|
`ScopeCompileTimeSafetyScala3Spec`.
|
|
270
354
|
|
|
271
|
-
### B)
|
|
355
|
+
### B) Opaque types prevent escape
|
|
272
356
|
|
|
273
|
-
|
|
357
|
+
Each scope defines its own `$[A]` opaque type. Even though `$[A] = A` at runtime, the compiler treats each scope's `$[A]` as distinct. A child's `$[Database]` is a different type than the parent's `$[Database]`.
|
|
358
|
+
|
|
359
|
+
Additionally, the opaque type hides `A`'s methods at the type level — you can't call `db.query(...)` directly on a `$[Database]`. Access routes are `scope.use(value)(f)` and the `ScopedOps` methods (`map`, `flatMap`) for for-comprehensions.
|
|
274
360
|
|
|
275
361
|
---
|
|
276
362
|
|
|
@@ -287,12 +373,13 @@ final class FileHandle(path: String) extends AutoCloseable {
|
|
|
287
373
|
}
|
|
288
374
|
|
|
289
375
|
Scope.global.scoped { scope =>
|
|
290
|
-
|
|
376
|
+
import scope._
|
|
291
377
|
|
|
292
|
-
val
|
|
293
|
-
scope.$(h)(_.readAll())
|
|
378
|
+
val h: $[FileHandle] = allocate(Resource(new FileHandle("data.txt")))
|
|
294
379
|
|
|
295
|
-
|
|
380
|
+
// scope.use applies function to scoped value, returns $[String]
|
|
381
|
+
val contents: $[String] = scope.use(h)(_.readAll())
|
|
382
|
+
println(contents) // $[String] = String at runtime
|
|
296
383
|
}
|
|
297
384
|
```
|
|
298
385
|
|
|
@@ -304,64 +391,84 @@ Scope.global.scoped { scope =>
|
|
|
304
391
|
import zio.blocks.scope._
|
|
305
392
|
|
|
306
393
|
Scope.global.scoped { parent =>
|
|
307
|
-
|
|
394
|
+
import parent._
|
|
395
|
+
|
|
396
|
+
val parentDb: $[Database] = allocate(Resource(new Database))
|
|
397
|
+
|
|
398
|
+
scoped { child =>
|
|
399
|
+
import child._
|
|
308
400
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
println(ok)
|
|
401
|
+
// Use lower() to access parent-scoped values in child scope:
|
|
402
|
+
val db: $[Database] = lower(parentDb)
|
|
403
|
+
println(child.use(db)(_.query("SELECT 1")))
|
|
313
404
|
|
|
314
|
-
val childDb =
|
|
405
|
+
val childDb: $[Database] = allocate(Resource(new Database))
|
|
315
406
|
|
|
316
407
|
// You can use childDb *inside* the child:
|
|
317
|
-
|
|
318
|
-
println(ok2)
|
|
408
|
+
println(child.use(childDb)(_.query("SELECT 2")))
|
|
319
409
|
|
|
320
|
-
// But you cannot return childDb to the parent
|
|
321
|
-
//
|
|
322
|
-
|
|
410
|
+
// But you cannot return childDb to the parent:
|
|
411
|
+
// $[Database] has no Unscoped instance — compile error
|
|
412
|
+
|
|
413
|
+
// Return an Unscoped value
|
|
414
|
+
"done"
|
|
323
415
|
}
|
|
324
416
|
|
|
325
417
|
// parentDb is still usable here:
|
|
326
|
-
|
|
327
|
-
println(stillOk)
|
|
418
|
+
println(parent.use(parentDb)(_.query("SELECT 3")))
|
|
328
419
|
}
|
|
329
420
|
```
|
|
330
421
|
|
|
331
422
|
---
|
|
332
423
|
|
|
333
|
-
###
|
|
424
|
+
### Chaining resource acquisition
|
|
425
|
+
|
|
426
|
+
Since `$[A]` supports `map` and `flatMap` via `ScopedOps`, you can chain resource acquisitions in for-comprehensions:
|
|
334
427
|
|
|
335
428
|
```scala
|
|
336
429
|
import zio.blocks.scope._
|
|
337
430
|
|
|
431
|
+
class Pool extends AutoCloseable {
|
|
432
|
+
def lease(): Connection = new Connection
|
|
433
|
+
def close(): Unit = println("pool closed")
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
class Connection extends AutoCloseable {
|
|
437
|
+
def query(sql: String): String = s"result: $sql"
|
|
438
|
+
def close(): Unit = println("connection closed")
|
|
439
|
+
}
|
|
440
|
+
|
|
338
441
|
Scope.global.scoped { scope =>
|
|
339
|
-
|
|
442
|
+
import scope._
|
|
340
443
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
444
|
+
// Chain allocations in a for-comprehension:
|
|
445
|
+
// flatMap unwraps $[Pool] to Pool, so pool.lease() returns a raw Connection
|
|
446
|
+
val result: $[String] = for {
|
|
447
|
+
pool <- allocate(Resource.from[Pool])
|
|
448
|
+
conn <- allocate(Resource(pool.lease()))
|
|
449
|
+
} yield conn.query("SELECT 1")
|
|
346
450
|
|
|
347
|
-
val result: String = scope(program)
|
|
348
451
|
println(result)
|
|
349
452
|
}
|
|
453
|
+
// Output: result: SELECT 1
|
|
454
|
+
// Then: connection closed, pool closed (LIFO)
|
|
350
455
|
```
|
|
351
456
|
|
|
352
457
|
---
|
|
353
458
|
|
|
354
459
|
### Registering cleanup manually with `defer`
|
|
355
460
|
|
|
356
|
-
Use `
|
|
461
|
+
Use `defer` when you already have a value and just need to register cleanup.
|
|
357
462
|
|
|
358
463
|
```scala
|
|
359
464
|
import zio.blocks.scope._
|
|
360
465
|
|
|
361
466
|
Scope.global.scoped { scope =>
|
|
467
|
+
import scope._
|
|
468
|
+
|
|
362
469
|
val handle = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
|
|
363
470
|
|
|
364
|
-
|
|
471
|
+
defer { handle.close() }
|
|
365
472
|
|
|
366
473
|
val firstByte = handle.read()
|
|
367
474
|
println(firstByte)
|
|
@@ -374,7 +481,9 @@ There is also a package-level helper `defer` that only requires a `Finalizer`:
|
|
|
374
481
|
import zio.blocks.scope._
|
|
375
482
|
|
|
376
483
|
Scope.global.scoped { scope =>
|
|
484
|
+
import scope._
|
|
377
485
|
given Finalizer = scope
|
|
486
|
+
|
|
378
487
|
defer { println("cleanup") }
|
|
379
488
|
}
|
|
380
489
|
```
|
|
@@ -390,7 +499,7 @@ import zio.blocks.scope._
|
|
|
390
499
|
|
|
391
500
|
class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
|
|
392
501
|
private val pool = createPool(config)
|
|
393
|
-
finalizer.defer { pool.shutdown() }
|
|
502
|
+
finalizer.defer { pool.shutdown() } // or: defer { ... } with import zio.blocks.scope._
|
|
394
503
|
|
|
395
504
|
def getConnection(): Connection = pool.acquire()
|
|
396
505
|
}
|
|
@@ -399,37 +508,40 @@ class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
|
|
|
399
508
|
val resource = Resource.from[ConnectionPool](Wire(Config("jdbc://localhost")))
|
|
400
509
|
|
|
401
510
|
Scope.global.scoped { scope =>
|
|
402
|
-
|
|
511
|
+
import scope._
|
|
512
|
+
val pool = allocate(resource)
|
|
403
513
|
// pool.shutdown() will be called when scope closes
|
|
404
514
|
}
|
|
405
515
|
```
|
|
406
516
|
|
|
407
517
|
Why `Finalizer` instead of `Scope`?
|
|
408
518
|
- `Finalizer` is the minimal interface—it only has `defer`
|
|
409
|
-
- Classes that need cleanup should not have access to `allocate` or
|
|
519
|
+
- Classes that need cleanup should not have access to `allocate` or `use`
|
|
410
520
|
- The macros pass a `Finalizer` at runtime, so declaring `Scope` would be misleading
|
|
411
521
|
|
|
412
522
|
---
|
|
413
523
|
|
|
414
524
|
### Dependency injection with `Wire` + `Context`
|
|
415
525
|
|
|
526
|
+
For manual wiring (when you already have dependencies assembled), use `wire.toResource(ctx)`:
|
|
527
|
+
|
|
416
528
|
```scala
|
|
417
529
|
import zio.blocks.scope._
|
|
418
530
|
import zio.blocks.context.Context
|
|
419
531
|
|
|
420
532
|
final case class Config(debug: Boolean)
|
|
421
533
|
|
|
422
|
-
val w: Wire.Shared[Boolean, Config] = shared[Config]
|
|
534
|
+
val w: Wire.Shared[Boolean, Config] = Wire.shared[Config]
|
|
423
535
|
val deps: Context[Boolean] = Context[Boolean](true)
|
|
424
536
|
|
|
425
537
|
Scope.global.scoped { scope =>
|
|
426
|
-
|
|
427
|
-
scope.allocate(w.toResource(deps))
|
|
538
|
+
import scope._
|
|
428
539
|
|
|
429
|
-
val
|
|
430
|
-
scope.$(cfg)(_.debug) // Boolean typically escapes
|
|
540
|
+
val cfg: $[Config] = allocate(w.toResource(deps))
|
|
431
541
|
|
|
432
|
-
|
|
542
|
+
val debug: $[Boolean] = scope.use(cfg)(_.debug)
|
|
543
|
+
|
|
544
|
+
println(debug) // $[Boolean] = Boolean at runtime
|
|
433
545
|
}
|
|
434
546
|
```
|
|
435
547
|
|
|
@@ -438,107 +550,120 @@ Sharing vs uniqueness at the wire level:
|
|
|
438
550
|
```scala
|
|
439
551
|
import zio.blocks.scope._
|
|
440
552
|
|
|
441
|
-
val ws = shared[Config] // shared recipe; sharing happens via Resource.Shared when allocated
|
|
442
|
-
val wu = unique[Config] // unique recipe; each allocation is fresh
|
|
553
|
+
val ws = Wire.shared[Config] // shared recipe; sharing happens via Resource.Shared when allocated
|
|
554
|
+
val wu = Wire.unique[Config] // unique recipe; each allocation is fresh
|
|
443
555
|
```
|
|
444
556
|
|
|
445
557
|
---
|
|
446
558
|
|
|
447
|
-
###
|
|
559
|
+
### Dependency injection with `Resource.from[T](wires*)`
|
|
448
560
|
|
|
449
|
-
`Resource.from[T]`
|
|
561
|
+
`Resource.from[T](wires*)` is the **primary entry point** for dependency injection. It resolves `T` and all its dependencies into a single `Resource[T]`.
|
|
450
562
|
|
|
451
563
|
```scala
|
|
452
564
|
import zio.blocks.scope._
|
|
453
|
-
import zio.blocks.context.Context
|
|
454
565
|
|
|
455
566
|
final case class Config(url: String)
|
|
456
567
|
|
|
457
|
-
|
|
458
|
-
def info(msg: String): Unit
|
|
568
|
+
final class Logger {
|
|
569
|
+
def info(msg: String): Unit = println(msg)
|
|
459
570
|
}
|
|
460
571
|
|
|
461
|
-
final class
|
|
462
|
-
def
|
|
572
|
+
final class Database(cfg: Config) extends AutoCloseable {
|
|
573
|
+
def query(sql: String): String = s"[${cfg.url}] $sql"
|
|
574
|
+
def close(): Unit = println("database closed")
|
|
463
575
|
}
|
|
464
576
|
|
|
465
|
-
final class Service(
|
|
466
|
-
def run(): Unit = logger.info(s"running with ${
|
|
577
|
+
final class Service(db: Database, logger: Logger) extends AutoCloseable {
|
|
578
|
+
def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
|
|
467
579
|
def close(): Unit = println("service closed")
|
|
468
580
|
}
|
|
469
581
|
|
|
470
|
-
//
|
|
582
|
+
// Only provide leaf values (primitives, configs) - the rest is auto-wired:
|
|
471
583
|
val serviceResource: Resource[Service] =
|
|
472
584
|
Resource.from[Service](
|
|
473
|
-
Wire(Config("jdbc:postgresql://localhost/db"))
|
|
474
|
-
Wire(new ConsoleLogger: Logger)
|
|
585
|
+
Wire(Config("jdbc:postgresql://localhost/db"))
|
|
475
586
|
)
|
|
476
587
|
|
|
477
588
|
Scope.global.scoped { scope =>
|
|
478
|
-
|
|
479
|
-
|
|
589
|
+
import scope._
|
|
590
|
+
val svc: $[Service] = allocate(serviceResource)
|
|
591
|
+
scope.use(svc)(_.run())
|
|
480
592
|
}
|
|
593
|
+
// Output: running with [jdbc:postgresql://localhost/db] SELECT 1
|
|
594
|
+
// Then: service closed, database closed (LIFO order)
|
|
481
595
|
```
|
|
482
596
|
|
|
483
|
-
|
|
484
|
-
-
|
|
485
|
-
-
|
|
597
|
+
**What you must provide:**
|
|
598
|
+
- Leaf values: primitives, configs, pre-existing instances via `Wire(value)`
|
|
599
|
+
- Abstract types: traits/abstract classes via `Wire.shared[ConcreteImpl]`
|
|
600
|
+
- Overrides: when you want `unique` instead of the default `shared`
|
|
601
|
+
|
|
602
|
+
**What is auto-created:**
|
|
603
|
+
- Concrete classes with accessible primary constructors (default: `Wire.shared`)
|
|
486
604
|
|
|
487
605
|
---
|
|
488
606
|
|
|
489
|
-
###
|
|
607
|
+
### Injecting traits via subtype wires
|
|
490
608
|
|
|
491
|
-
When
|
|
609
|
+
When a dependency is a trait or abstract class, provide a wire for a concrete implementation:
|
|
492
610
|
|
|
493
611
|
```scala
|
|
494
612
|
import zio.blocks.scope._
|
|
495
|
-
import zio.blocks.context.Context
|
|
496
613
|
|
|
497
|
-
trait
|
|
498
|
-
def
|
|
614
|
+
trait Logger {
|
|
615
|
+
def info(msg: String): Unit
|
|
499
616
|
}
|
|
500
617
|
|
|
501
|
-
final class
|
|
502
|
-
def
|
|
503
|
-
def close(): Unit = println("LiveDatabaseApi closed")
|
|
618
|
+
final class ConsoleLogger extends Logger {
|
|
619
|
+
def info(msg: String): Unit = println(msg)
|
|
504
620
|
}
|
|
505
621
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
given Wireable.Typed[Config, DatabaseApi] =
|
|
509
|
-
Wireable.fromWire(shared[LiveDatabaseApi].shared.asInstanceOf[Wire[Config, DatabaseApi]])
|
|
622
|
+
final class App(logger: Logger) {
|
|
623
|
+
def run(): Unit = logger.info("Hello!")
|
|
510
624
|
}
|
|
511
625
|
|
|
512
|
-
|
|
626
|
+
// Wire.shared[ConsoleLogger] satisfies the Logger dependency via subtyping:
|
|
627
|
+
val appResource: Resource[App] =
|
|
628
|
+
Resource.from[App](
|
|
629
|
+
Wire.shared[ConsoleLogger]
|
|
630
|
+
)
|
|
513
631
|
|
|
514
632
|
Scope.global.scoped { scope =>
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
scope.allocate(shared[DatabaseApi].toResource(deps))
|
|
519
|
-
|
|
520
|
-
val out: String =
|
|
521
|
-
scope.$(db)(_.query("SELECT 1"))
|
|
522
|
-
|
|
523
|
-
println(out)
|
|
633
|
+
import scope._
|
|
634
|
+
val app: $[App] = allocate(appResource)
|
|
635
|
+
scope.use(app)(_.run())
|
|
524
636
|
}
|
|
525
637
|
```
|
|
526
638
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
639
|
+
**Single instance for diamond patterns:**
|
|
640
|
+
|
|
641
|
+
```scala
|
|
642
|
+
trait Service
|
|
643
|
+
class LiveService extends Service
|
|
644
|
+
class NeedsService(s: Service)
|
|
645
|
+
class NeedsLive(l: LiveService)
|
|
646
|
+
class App(a: NeedsService, b: NeedsLive)
|
|
647
|
+
|
|
648
|
+
// One LiveService instance satisfies both Service and LiveService dependencies:
|
|
649
|
+
val appResource = Resource.from[App](
|
|
650
|
+
Wire.shared[LiveService]
|
|
651
|
+
)
|
|
652
|
+
// count of LiveService instantiations: 1
|
|
653
|
+
```
|
|
530
654
|
|
|
531
655
|
---
|
|
532
656
|
|
|
533
657
|
### Interop escape hatch: `leak`
|
|
534
658
|
|
|
535
|
-
Sometimes you must hand a raw value to code that cannot work with
|
|
659
|
+
Sometimes you must hand a raw value to code that cannot work with `$[A]` types.
|
|
536
660
|
|
|
537
661
|
```scala
|
|
538
662
|
import zio.blocks.scope._
|
|
539
663
|
|
|
540
664
|
Scope.global.scoped { scope =>
|
|
541
|
-
|
|
665
|
+
import scope._
|
|
666
|
+
val db: $[Database] = allocate(Resource(new Database))
|
|
542
667
|
|
|
543
668
|
val raw: Database = leak(db) // emits a compiler warning
|
|
544
669
|
// thirdParty(raw)
|
|
@@ -549,6 +674,168 @@ Scope.global.scoped { scope =>
|
|
|
549
674
|
|
|
550
675
|
---
|
|
551
676
|
|
|
677
|
+
## Common compile errors
|
|
678
|
+
|
|
679
|
+
The scope macros produce beautiful, actionable compile-time error messages with ASCII diagrams and helpful hints:
|
|
680
|
+
|
|
681
|
+
### Not a class (Wire.shared/unique on trait or abstract class)
|
|
682
|
+
|
|
683
|
+
```
|
|
684
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
685
|
+
|
|
686
|
+
Cannot derive Wire for MyTrait: not a class.
|
|
687
|
+
|
|
688
|
+
Hint: Use Wire.Shared / Wire.Unique directly.
|
|
689
|
+
|
|
690
|
+
────────────────────────────────────────────────────────────────────────────
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
### Unmakeable type (primitives, functions, collections)
|
|
694
|
+
|
|
695
|
+
```
|
|
696
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
697
|
+
|
|
698
|
+
Cannot auto-create String
|
|
699
|
+
|
|
700
|
+
This type (primitive, collection, or function) cannot be auto-created.
|
|
701
|
+
|
|
702
|
+
Required by:
|
|
703
|
+
├── Config
|
|
704
|
+
└── App
|
|
705
|
+
|
|
706
|
+
Fix: Provide Wire(value) with the desired value:
|
|
707
|
+
|
|
708
|
+
Resource.from[...](
|
|
709
|
+
Wire(...), // provide a value for String
|
|
710
|
+
...
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
────────────────────────────────────────────────────────────────────────────
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Abstract type (trait or abstract class)
|
|
717
|
+
|
|
718
|
+
```
|
|
719
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
720
|
+
|
|
721
|
+
Cannot auto-create Logger
|
|
722
|
+
|
|
723
|
+
This type is abstract (trait or abstract class).
|
|
724
|
+
|
|
725
|
+
Required by:
|
|
726
|
+
└── App
|
|
727
|
+
|
|
728
|
+
Fix: Provide a wire for a concrete implementation:
|
|
729
|
+
|
|
730
|
+
Resource.from[...](
|
|
731
|
+
Wire.shared[ConcreteImpl], // provides Logger
|
|
732
|
+
...
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
────────────────────────────────────────────────────────────────────────────
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
### Duplicate providers (ambiguous wires)
|
|
739
|
+
|
|
740
|
+
```
|
|
741
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
742
|
+
|
|
743
|
+
Multiple providers for Service
|
|
744
|
+
|
|
745
|
+
Conflicting wires:
|
|
746
|
+
1. LiveService
|
|
747
|
+
2. TestService
|
|
748
|
+
|
|
749
|
+
Hint: Remove duplicate wires or use distinct wrapper types.
|
|
750
|
+
|
|
751
|
+
────────────────────────────────────────────────────────────────────────────
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Dependency cycle
|
|
755
|
+
|
|
756
|
+
```
|
|
757
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
Dependency cycle detected
|
|
760
|
+
|
|
761
|
+
Cycle:
|
|
762
|
+
┌───────────┐
|
|
763
|
+
│ ▼
|
|
764
|
+
A ──► B ──► C
|
|
765
|
+
▲ │
|
|
766
|
+
└───────────┘
|
|
767
|
+
|
|
768
|
+
Break the cycle by:
|
|
769
|
+
• Introducing an interface/trait
|
|
770
|
+
• Using lazy initialization
|
|
771
|
+
• Restructuring dependencies
|
|
772
|
+
|
|
773
|
+
────────────────────────────────────────────────────────────────────────────
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
### Subtype conflict (related dependency types)
|
|
777
|
+
|
|
778
|
+
```
|
|
779
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
780
|
+
|
|
781
|
+
Dependency type conflict in MyService
|
|
782
|
+
|
|
783
|
+
FileInputStream is a subtype of InputStream.
|
|
784
|
+
|
|
785
|
+
When both types are dependencies, Context cannot reliably distinguish
|
|
786
|
+
them. The more specific type may be retrieved when the more general
|
|
787
|
+
type is requested.
|
|
788
|
+
|
|
789
|
+
To fix this, wrap one or both types in a distinct wrapper:
|
|
790
|
+
|
|
791
|
+
case class WrappedInputStream(value: InputStream)
|
|
792
|
+
or
|
|
793
|
+
opaque type WrappedInputStream = InputStream
|
|
794
|
+
|
|
795
|
+
────────────────────────────────────────────────────────────────────────────
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Duplicate parameter types in constructor
|
|
799
|
+
|
|
800
|
+
```
|
|
801
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
802
|
+
|
|
803
|
+
Constructor of App has multiple parameters of type String
|
|
804
|
+
|
|
805
|
+
Context is type-indexed and cannot supply distinct values for the same type.
|
|
806
|
+
|
|
807
|
+
Fix: Wrap one parameter in an opaque type to distinguish them:
|
|
808
|
+
|
|
809
|
+
opaque type FirstString = String
|
|
810
|
+
or
|
|
811
|
+
case class FirstString(value: String)
|
|
812
|
+
|
|
813
|
+
────────────────────────────────────────────────────────────────────────────
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### Leak warning
|
|
817
|
+
|
|
818
|
+
When using `leak(value)` to escape the scoped type system:
|
|
819
|
+
|
|
820
|
+
```
|
|
821
|
+
── Scope Warning ────────────────────────────────────────────────────────────
|
|
822
|
+
|
|
823
|
+
leak(db)
|
|
824
|
+
^
|
|
825
|
+
|
|
|
826
|
+
|
|
827
|
+
Warning: db is being leaked from scope MyScope.
|
|
828
|
+
This may result in undefined behavior.
|
|
829
|
+
|
|
830
|
+
Hint:
|
|
831
|
+
If you know this data type is not resourceful, then add an Unscoped
|
|
832
|
+
instance for it so you do not need to leak it.
|
|
833
|
+
|
|
834
|
+
────────────────────────────────────────────────────────────────────────────
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
---
|
|
838
|
+
|
|
552
839
|
## API reference (selected)
|
|
553
840
|
|
|
554
841
|
### `Scope`
|
|
@@ -556,24 +843,42 @@ Scope.global.scoped { scope =>
|
|
|
556
843
|
Core methods (Scala 3 `using` vs Scala 2 `implicit` differs, but the shapes are the same):
|
|
557
844
|
|
|
558
845
|
```scala
|
|
559
|
-
|
|
560
|
-
type
|
|
846
|
+
sealed abstract class Scope extends Finalizer {
|
|
847
|
+
type $[+A] // = A at runtime (zero-cost)
|
|
848
|
+
type Parent <: Scope
|
|
849
|
+
val parent: Parent
|
|
561
850
|
|
|
562
|
-
def allocate[A](resource: Resource[A]): A
|
|
851
|
+
def allocate[A](resource: Resource[A]): $[A]
|
|
852
|
+
def allocate[A <: AutoCloseable](value: => A): $[A]
|
|
563
853
|
def defer(f: => Unit): Unit
|
|
564
854
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
855
|
+
// Apply function to scoped value, returns scoped result
|
|
856
|
+
def use[A, B](scoped: $[A])(f: A => B): $[B]
|
|
857
|
+
|
|
858
|
+
// Construct a scoped value from a raw value
|
|
859
|
+
def $[A](a: A): $[A]
|
|
860
|
+
|
|
861
|
+
// Lower parent-scoped value into this scope
|
|
862
|
+
def lower[A](value: parent.$[A]): $[A]
|
|
863
|
+
|
|
864
|
+
// Escape hatch: unwrap scoped value (emits compiler warning)
|
|
865
|
+
// Scala 3:
|
|
866
|
+
inline def leak[A](inline sa: $[A]): A // macro — emits warning
|
|
867
|
+
// Scala 2:
|
|
868
|
+
def leak[A](sa: $[A]): A // macro — emits warning
|
|
869
|
+
|
|
870
|
+
// Creates a child scope - requires Unscoped evidence on return type
|
|
871
|
+
// Scala 3:
|
|
872
|
+
def scoped[A](f: (child: Scope.Child[self.type]) => child.$[A])(using Unscoped[A]): A
|
|
873
|
+
// Scala 2 (macro rewrites the types; declared signature is untyped):
|
|
874
|
+
def scoped(f: Scope.Child[self.type] => Any): Any // macro
|
|
569
875
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
876
|
+
implicit class ScopedOps[A](sa: $[A]) {
|
|
877
|
+
def map[B](f: A => B): $[B]
|
|
878
|
+
def flatMap[B](f: A => $[B]): $[B]
|
|
879
|
+
}
|
|
574
880
|
|
|
575
|
-
|
|
576
|
-
def scoped[A](f: Scope[this.Tag, ? <: this.Tag] => A): A
|
|
881
|
+
implicit def wrapUnscoped[A: Unscoped](a: A): $[A]
|
|
577
882
|
}
|
|
578
883
|
```
|
|
579
884
|
|
|
@@ -587,20 +892,17 @@ object Resource {
|
|
|
587
892
|
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
|
|
588
893
|
def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
|
|
589
894
|
|
|
590
|
-
// Macro-
|
|
591
|
-
def from[T]: Resource[T]
|
|
592
|
-
def from[T](wires: Wire[?, ?]*): Resource[T]
|
|
593
|
-
|
|
594
|
-
// Internal / produced by wires:
|
|
595
|
-
def shared[A](f: Finalizer => A): Resource.Shared[A]
|
|
596
|
-
def unique[A](f: Finalizer => A): Resource.Unique[A]
|
|
895
|
+
// Macro - DI entry point:
|
|
896
|
+
def from[T]: Resource[T] // zero-dep classes
|
|
897
|
+
def from[T](wires: Wire[?, ?]*): Resource[T] // with dependency wires
|
|
597
898
|
|
|
598
|
-
|
|
599
|
-
|
|
899
|
+
// Internal (used by generated code):
|
|
900
|
+
def shared[A](f: Finalizer => A): Resource[A]
|
|
901
|
+
def unique[A](f: Finalizer => A): Resource[A]
|
|
600
902
|
}
|
|
601
903
|
```
|
|
602
904
|
|
|
603
|
-
### `Wire`
|
|
905
|
+
### `Wire`
|
|
604
906
|
|
|
605
907
|
```scala
|
|
606
908
|
sealed trait Wire[-In, +Out] {
|
|
@@ -610,9 +912,16 @@ sealed trait Wire[-In, +Out] {
|
|
|
610
912
|
def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
|
|
611
913
|
}
|
|
612
914
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
def
|
|
915
|
+
object Wire {
|
|
916
|
+
// Macro entry points:
|
|
917
|
+
def shared[T]: Wire.Shared[?, T] // derive from T's constructor
|
|
918
|
+
def unique[T]: Wire.Unique[?, T] // derive from T's constructor
|
|
919
|
+
|
|
920
|
+
// Wrap pre-existing value (auto-finalizes if AutoCloseable):
|
|
921
|
+
def apply[T](value: T): Wire.Shared[Any, T]
|
|
922
|
+
|
|
923
|
+
final case class Shared[-In, +Out] extends Wire[In, Out]
|
|
924
|
+
final case class Unique[-In, +Out] extends Wire[In, Out]
|
|
616
925
|
}
|
|
617
926
|
```
|
|
618
927
|
|
|
@@ -620,8 +929,17 @@ trait Wireable[+Out] {
|
|
|
620
929
|
|
|
621
930
|
## Mental model recap
|
|
622
931
|
|
|
623
|
-
- Use `Scope.global.scoped { scope => ... }` to create a safe region.
|
|
624
|
-
-
|
|
625
|
-
-
|
|
626
|
-
-
|
|
932
|
+
- Use `Scope.global.scoped { scope => import scope._; ... }` to create a safe region.
|
|
933
|
+
- For simple resources: `allocate(Resource(value))` or `allocate(Resource.acquireRelease(...)(...))`
|
|
934
|
+
- For dependency injection: `allocate(Resource.from[App](Wire(config), ...))` — auto-wires concrete classes, you provide leaves and overrides.
|
|
935
|
+
- Use `scope.use(value)(f)` to work with scoped values — all operations are eager.
|
|
936
|
+
- `$[A] = A` at runtime — zero-cost opaque type.
|
|
937
|
+
- The `scoped` method requires `Unscoped[A]` evidence on the return type.
|
|
938
|
+
- Use `lower(parentValue)` to access parent-scoped values in child scopes.
|
|
939
|
+
- Return `Unscoped` types from child scopes to extract raw values.
|
|
627
940
|
- If it doesn't typecheck, it would have been unsafe at runtime.
|
|
941
|
+
|
|
942
|
+
**The 3 macro entry points:**
|
|
943
|
+
- `Wire.shared[T]` — shared wire from constructor
|
|
944
|
+
- `Wire.unique[T]` — unique wire from constructor
|
|
945
|
+
- `Resource.from[T](wires*)` — wire up T and all dependencies
|