@zio.dev/zio-blocks 0.0.29 → 0.0.30
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/guides/compile-time-resource-safety-with-scope.md +997 -0
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +203 -203
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +20 -14
- package/package.json +1 -1
- package/reference/codec.md +7 -7
- package/reference/context.md +1 -1
- package/reference/docs.md +1 -1
- package/reference/dynamic-schema.md +39 -42
- package/reference/media-type.md +2 -2
- package/reference/resource-management/defer-handle.md +246 -0
- package/reference/resource-management/finalization.md +286 -0
- package/reference/resource-management/finalizer.md +167 -0
- package/reference/{resource-management-di → resource-management}/index.md +3 -8
- package/reference/{resource-management-di → resource-management}/resource.md +532 -50
- package/reference/resource-management/scope.md +3026 -0
- package/reference/resource-management/unscoped.md +125 -0
- package/reference/{resource-management-di → resource-management}/wire.md +74 -28
- package/reference/schema-evolution/as.md +4 -4
- package/reference/schema-evolution/into.md +2 -2
- package/reference/schema-expr.md +2 -2
- package/reference/streams.md +989 -0
- package/reference/type-class-derivation.md +1 -1
- package/ringbuffer.md +1 -1
- package/sidebars.js +9 -4
- package/reference/resource-management-di/scope.md +0 -1423
|
@@ -1,1423 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
id: scope
|
|
3
|
-
title: "Scope"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
`zio.blocks.scope` is a **compile-time safe, zero-cost** resource management library for **Scala 3** (and Scala 2.13). It prevents a large class of lifetime bugs by tagging allocated values with an *unnameable*, scope-specific type and restricting how those values may be used.
|
|
7
|
-
|
|
8
|
-
At runtime the model stays simple:
|
|
9
|
-
|
|
10
|
-
- **Allocate eagerly** (no lazy thunks)
|
|
11
|
-
- **Register finalizers**
|
|
12
|
-
- **Run finalizers deterministically** when a scope closes (**LIFO** order)
|
|
13
|
-
- Collect finalizer failures into a `Finalization` (and throw/suppress appropriately)
|
|
14
|
-
|
|
15
|
-
## Why Scope?
|
|
16
|
-
|
|
17
|
-
Most resource bugs in Scala are "escape" bugs:
|
|
18
|
-
|
|
19
|
-
- storing a connection/stream in a field and using it after it was closed
|
|
20
|
-
- capturing a resource in a closure that outlives a scope
|
|
21
|
-
- passing a resource to code that might retain it
|
|
22
|
-
- mixing values from different lifetimes ("which scope owns this?")
|
|
23
|
-
|
|
24
|
-
Scope addresses these with a *tight* design:
|
|
25
|
-
|
|
26
|
-
| Feature | `zio.blocks.scope` |
|
|
27
|
-
|---|---|
|
|
28
|
-
| Compile-time leak prevention | ✓ (`scope.$[A]` + `$` macro + `Unscoped` boundary) |
|
|
29
|
-
| Runtime overhead | ~0 (scoped values erase to `A`) |
|
|
30
|
-
| Allocation model | Eager (allocation happens at `allocate`) |
|
|
31
|
-
| Finalization | Deterministic, LIFO, errors collected |
|
|
32
|
-
| Structured lifetime | Parent/child scopes, `lower` for explicit lifetime widening |
|
|
33
|
-
| Escape hatch | `leak` (warns) |
|
|
34
|
-
|
|
35
|
-
If you've used `try/finally`, `Using`, or ZIO's `Scope`, this is the same problem space—but optimized for **synchronous code** with **compile-time boundaries**.
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Quick start (Scala 3)
|
|
40
|
-
|
|
41
|
-
```scala
|
|
42
|
-
import zio.blocks.scope.*
|
|
43
|
-
|
|
44
|
-
final class Database extends AutoCloseable:
|
|
45
|
-
def query(sql: String): String = s"result: $sql"
|
|
46
|
-
def close(): Unit = println("db closed")
|
|
47
|
-
|
|
48
|
-
@main def quickStart(): Unit =
|
|
49
|
-
val out: String =
|
|
50
|
-
Scope.global.scoped { scope =>
|
|
51
|
-
import scope.*
|
|
52
|
-
|
|
53
|
-
val db: $[Database] =
|
|
54
|
-
Resource.fromAutoCloseable(new Database).allocate
|
|
55
|
-
|
|
56
|
-
// Safe access: the lambda parameter can only be used as a receiver
|
|
57
|
-
$(db)(_.query("SELECT 1"))
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
println(out)
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Key points:
|
|
64
|
-
|
|
65
|
-
- `allocate(...)` returns a **scoped value**: `scope.$[Database]` (or `$[Database]` after `import scope.*`).
|
|
66
|
-
- You **cannot** call `db.query(...)` directly on `$[Database]`.
|
|
67
|
-
- You use the `$` access operator: `$(db)(...)` (or `(scope $ db)(...)` without the import).
|
|
68
|
-
- The `scoped` block returns a plain `String` because `String: Unscoped`.
|
|
69
|
-
- Finalizers run when the block exits, in **LIFO** order.
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
73
|
-
## Core mental model
|
|
74
|
-
|
|
75
|
-
### 1) `Scope`: finalizers + type identity
|
|
76
|
-
|
|
77
|
-
`Scope` is a finalizer registry plus a unique type identity:
|
|
78
|
-
|
|
79
|
-
- `type $[+A]` — a scope-tagged, path-dependent type (erases to `A` at runtime)
|
|
80
|
-
- `type Parent <: Scope` / `val parent: Parent` — the scope hierarchy
|
|
81
|
-
|
|
82
|
-
Every scope instance defines a **different** `$` type, so values from different scopes don't accidentally mix.
|
|
83
|
-
|
|
84
|
-
```scala
|
|
85
|
-
Scope.global.scoped { scope =>
|
|
86
|
-
import scope.*
|
|
87
|
-
val x: $[Int] = 1 // ok (in global, $[A] = A)
|
|
88
|
-
}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
#### Global scope
|
|
92
|
-
|
|
93
|
-
`Scope.global` is the root:
|
|
94
|
-
|
|
95
|
-
- In the global scope: `type $[+A] = A` (identity)
|
|
96
|
-
- On the JVM: global finalizers run on shutdown via a shutdown hook
|
|
97
|
-
- On Scala.js: there is no shutdown hook, so global finalizers are **not** run automatically
|
|
98
|
-
|
|
99
|
-
---
|
|
100
|
-
|
|
101
|
-
### 2) Scoped values: `scope.$[A]` / `$[A]`
|
|
102
|
-
|
|
103
|
-
A value of type `scope.$[A]` means:
|
|
104
|
-
|
|
105
|
-
> "This is an `A`, but it is only valid while `scope` is alive."
|
|
106
|
-
|
|
107
|
-
Properties:
|
|
108
|
-
|
|
109
|
-
- **Zero-cost**: `$[A]` is just `A` at runtime (casts/identity)
|
|
110
|
-
- **Incompatible across scopes**: `outer.$[A]` is not `inner.$[A]`
|
|
111
|
-
- **Methods are hidden** at the type level; you must use `$` to access
|
|
112
|
-
|
|
113
|
-
#### Access operator: `(scope $ value)(f)`
|
|
114
|
-
|
|
115
|
-
The intended way to use a scoped value is:
|
|
116
|
-
|
|
117
|
-
```scala
|
|
118
|
-
(scope $ scopedValue)(a => a.method(...))
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
This is enforced by a macro that checks the lambda uses its parameter only in **receiver position**.
|
|
122
|
-
|
|
123
|
-
Allowed:
|
|
124
|
-
|
|
125
|
-
```scala
|
|
126
|
-
(scope $ db)(_.query("SELECT 1"))
|
|
127
|
-
(scope $ db)(d => d.query("a") + d.query("b"))
|
|
128
|
-
(scope $ db)(_.query("x").toUpperCase)
|
|
129
|
-
(scope $ db)(_.field) // field access is allowed
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Rejected at compile time:
|
|
133
|
-
|
|
134
|
-
```scala
|
|
135
|
-
(scope $ db)(d => store(d)) // parameter used as an argument
|
|
136
|
-
(scope $ db)(d => () => d.query("x")) // captured in a nested lambda
|
|
137
|
-
(scope $ db)(d => d) // returning the parameter
|
|
138
|
-
(scope $ db)(d => { val x = d; 1 }) // binding/storing the parameter itself
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
##### "Auto-unwrap" rule (`Unscoped`)
|
|
142
|
-
|
|
143
|
-
`$` *auto-unwraps* when the result type is known to be safe data:
|
|
144
|
-
|
|
145
|
-
- if `B: Unscoped` → `(scope $ sa)(f)` returns **`B`**
|
|
146
|
-
- otherwise → it returns **`scope.$[B]`**
|
|
147
|
-
|
|
148
|
-
```scala
|
|
149
|
-
Scope.global.scoped { scope =>
|
|
150
|
-
import scope.*
|
|
151
|
-
|
|
152
|
-
val db: $[Database] = Resource.from[Database].allocate
|
|
153
|
-
|
|
154
|
-
val s: String = $(db)(_.query("SELECT 1")) // String is Unscoped => unwrapped
|
|
155
|
-
val n: Int = $(db)(_.query("x").length) // Int is Unscoped => unwrapped
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
##### N-ary `$`: accessing multiple scoped values at once
|
|
160
|
-
|
|
161
|
-
When a result depends on **two or more** scoped values simultaneously, use the N-ary overloads (`N = 2..5`):
|
|
162
|
-
|
|
163
|
-
```scala
|
|
164
|
-
$(sa1, sa2)((v1, v2) => v1.method(v2.result()))
|
|
165
|
-
$(sa1, sa2, sa3)((v1, v2, v3) => v1.query(v2.key()) + v3.tag())
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
The same receiver-only grammar applies to every parameter: each `vi` may only appear as a method receiver (e.g., `vi.method()`). Feeding the *result* of one parameter to a method of another is permitted:
|
|
169
|
-
|
|
170
|
-
```scala
|
|
171
|
-
Scope.global.scoped { scope =>
|
|
172
|
-
import scope.*
|
|
173
|
-
val db: $[Database] = Resource.from[Database].allocate
|
|
174
|
-
val cache: $[Cache] = Resource.from[Cache].allocate
|
|
175
|
-
|
|
176
|
-
// d1 and d2 are both receivers; d2.key() produces a plain String arg
|
|
177
|
-
val result: String = $(db, cache)((d1, d2) => d1.query(d2.key()))
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Rejected at compile time (same rules as N=1, applied to each parameter independently):
|
|
182
|
-
|
|
183
|
-
```scala
|
|
184
|
-
$(db, cache)((d1, d2) => d2) // d2 returned directly
|
|
185
|
-
$(db, cache)((d1, d2) => store(d1)) // d1 passed as argument
|
|
186
|
-
$(db, cache)((d1, d2) => d1.method(d2)) // d2 as bare arg (not a receiver)
|
|
187
|
-
$(db, cache)((d1, d2) => () => d2.query()) // d2 captured in closure
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
The error messages name the offending parameter:
|
|
191
|
-
|
|
192
|
-
```
|
|
193
|
-
Parameter 2 ('d2') cannot be passed as an argument to a function or method.
|
|
194
|
-
Scoped values may only be used as a method receiver (e.g., d2.method()).
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
**Infix syntax** (`scope $ sa`) is only available for N=1. For N≥2, use unqualified syntax after `import scope.*`:
|
|
198
|
-
|
|
199
|
-
```scala
|
|
200
|
-
$(db, cache)((d, c) => d.query(c.key())) // ✓ unqualified
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
**For N>5**, extract each value in sequence (all results are `Unscoped` strings/values and can be freely combined):
|
|
204
|
-
|
|
205
|
-
```scala
|
|
206
|
-
val q1 = $(db1)(_.query("a"))
|
|
207
|
-
val q2 = $(db2)(_.query("b"))
|
|
208
|
-
q1 + q2
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
### 3) `Resource[A]`: acquisition + finalization
|
|
214
|
-
|
|
215
|
-
A `Resource[A]` is a **lazy description** of how to acquire a value and register cleanup in a scope. Nothing happens until you call `scope.allocate(resource)` (or `.allocate` syntax).
|
|
216
|
-
|
|
217
|
-
#### Constructors
|
|
218
|
-
|
|
219
|
-
From the source:
|
|
220
|
-
|
|
221
|
-
- `Resource(value: => A)`
|
|
222
|
-
Wraps a by-name value; if it's `AutoCloseable`, `close()` is registered automatically (runtime check).
|
|
223
|
-
- `Resource.fromAutoCloseable(thunk: => A <: AutoCloseable)`
|
|
224
|
-
Type-safe helper that registers `close()`.
|
|
225
|
-
- `Resource.acquireRelease(acquire: => A)(release: A => Unit)`
|
|
226
|
-
- `Resource.shared(f: Scope => A)`
|
|
227
|
-
**Memoized + reference-counted**, thread-safe.
|
|
228
|
-
- `Resource.unique(f: Scope => A)`
|
|
229
|
-
Fresh instance per allocation.
|
|
230
|
-
- `Resource.from[T]` and `Resource.from[T](wires*)` (macros)
|
|
231
|
-
Constructor-based dependency injection (covered below).
|
|
232
|
-
|
|
233
|
-
#### Composition
|
|
234
|
-
|
|
235
|
-
`Resource` composes with:
|
|
236
|
-
|
|
237
|
-
- `map`
|
|
238
|
-
- `flatMap`
|
|
239
|
-
- `zip`
|
|
240
|
-
|
|
241
|
-
Finalizers remain tied to the allocation scope; in composed resources, finalizers still run LIFO.
|
|
242
|
-
|
|
243
|
-
#### Sharing vs uniqueness (important)
|
|
244
|
-
|
|
245
|
-
There are two distinct ideas:
|
|
246
|
-
|
|
247
|
-
1. **Uniqueness**: "each allocation yields a fresh instance"
|
|
248
|
-
- Use `Resource.unique(...)`, or most ordinary `Resource(...)` / `acquireRelease(...)` resources.
|
|
249
|
-
- Each `allocate` runs the acquisition again and registers an independent finalizer.
|
|
250
|
-
|
|
251
|
-
2. **Sharing**: "reusing the same instance across multiple allocations"
|
|
252
|
-
- Use `Resource.shared(...)` (or wires/resources that convert to shared).
|
|
253
|
-
- Sharing is tied to **reusing the same `Resource.Shared` value**, not "magic caching inside a scope".
|
|
254
|
-
- The first allocation initializes via an `OpenScope` parented to `Scope.global`; subsequent allocations increment a reference count. When the last referencing scope closes, the shared scope is closed.
|
|
255
|
-
|
|
256
|
-
---
|
|
257
|
-
|
|
258
|
-
### 4) `Unscoped[A]`: types that may escape a scope
|
|
259
|
-
|
|
260
|
-
`Unscoped[A]` is a marker typeclass for *pure data*. It's used in two places:
|
|
261
|
-
|
|
262
|
-
1. `Scope.scoped` requires `Unscoped[A]` for the block's result type
|
|
263
|
-
⇒ prevents returning resources, closures, or scoped values.
|
|
264
|
-
2. `$` auto-unwraps results of type `B` when `B: Unscoped`.
|
|
265
|
-
|
|
266
|
-
Built-in instances include primitives, `String`, many collections/containers, time values, `java.util.UUID`, and `zio.blocks.chunk.Chunk` (when element types are unscoped).
|
|
267
|
-
|
|
268
|
-
#### Deriving / defining your own instances
|
|
269
|
-
|
|
270
|
-
Scala 3 (derivation via `Unscoped.derived`):
|
|
271
|
-
|
|
272
|
-
```scala
|
|
273
|
-
import zio.blocks.scope.*
|
|
274
|
-
|
|
275
|
-
final case class Config(debug: Boolean)
|
|
276
|
-
object Config:
|
|
277
|
-
given Unscoped[Config] = Unscoped.derived
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
Scala 2.13:
|
|
281
|
-
|
|
282
|
-
```scala
|
|
283
|
-
import zio.blocks.scope.*
|
|
284
|
-
|
|
285
|
-
final case class Config(debug: Boolean)
|
|
286
|
-
object Config {
|
|
287
|
-
implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
#### Scope boundary example
|
|
292
|
-
|
|
293
|
-
```scala
|
|
294
|
-
import zio.blocks.scope.*
|
|
295
|
-
|
|
296
|
-
Scope.global.scoped { parent =>
|
|
297
|
-
import parent.*
|
|
298
|
-
|
|
299
|
-
val ok: String =
|
|
300
|
-
parent.scoped { child =>
|
|
301
|
-
"hello" // String is Unscoped
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Does not compile: returning a resourceful value from a scoped block
|
|
305
|
-
// val leaked: Database =
|
|
306
|
-
// parent.scoped { child =>
|
|
307
|
-
// import child.*
|
|
308
|
-
// Resource.fromAutoCloseable(new Database).allocate
|
|
309
|
-
// }
|
|
310
|
-
|
|
311
|
-
ok
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
---
|
|
316
|
-
|
|
317
|
-
### 5) `lower`: using a parent-scoped value in a child scope
|
|
318
|
-
|
|
319
|
-
Because each scope has its own `$[A]` type, a child cannot directly use a parent's `$[A]`. Use `lower` to retag a parent-scoped value into the child:
|
|
320
|
-
|
|
321
|
-
```scala
|
|
322
|
-
import zio.blocks.scope.*
|
|
323
|
-
|
|
324
|
-
Scope.global.scoped { outer =>
|
|
325
|
-
import outer.*
|
|
326
|
-
|
|
327
|
-
val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
328
|
-
|
|
329
|
-
outer.scoped { inner =>
|
|
330
|
-
import inner.*
|
|
331
|
-
val innerDb: $[Database] = lower(db)
|
|
332
|
-
$(innerDb)(_.query("child"))
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
This is safe because **parents always outlive children** (child finalizers run before the parent closes).
|
|
338
|
-
|
|
339
|
-
---
|
|
340
|
-
|
|
341
|
-
### 6) `defer`: manual finalizers (+ cancellation)
|
|
342
|
-
|
|
343
|
-
Use `defer` to register cleanup. It returns a `DeferHandle` you can cancel.
|
|
344
|
-
|
|
345
|
-
```scala
|
|
346
|
-
import zio.blocks.scope.*
|
|
347
|
-
|
|
348
|
-
Scope.global.scoped { scope =>
|
|
349
|
-
import scope.*
|
|
350
|
-
|
|
351
|
-
val in = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
|
|
352
|
-
|
|
353
|
-
val h: DeferHandle =
|
|
354
|
-
defer(in.close())
|
|
355
|
-
|
|
356
|
-
val first = in.read()
|
|
357
|
-
println(first)
|
|
358
|
-
|
|
359
|
-
// If you already cleaned up manually:
|
|
360
|
-
// h.cancel() // thread-safe, idempotent
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
There is also a **package-level** helper that only requires a `Finalizer`:
|
|
365
|
-
|
|
366
|
-
```scala
|
|
367
|
-
import zio.blocks.scope.*
|
|
368
|
-
|
|
369
|
-
Scope.global.scoped { scope =>
|
|
370
|
-
import scope.*
|
|
371
|
-
given Finalizer = scope
|
|
372
|
-
|
|
373
|
-
defer(println("cleanup")) // uses the package-level helper
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
---
|
|
378
|
-
|
|
379
|
-
### 7) `open()`: non-lexical, explicitly-managed child scopes
|
|
380
|
-
|
|
381
|
-
`scoped` ties lifetime to a block. `open()` creates a child scope you close explicitly.
|
|
382
|
-
|
|
383
|
-
- The child scope is **unowned** (can be used from any thread)
|
|
384
|
-
- Still **linked to the parent**: parent closing will also close the child
|
|
385
|
-
- You must call `close()` on the handle to detach + finalize now
|
|
386
|
-
|
|
387
|
-
From `Scope.global` the returned type is `Scope.OpenScope` directly (because global `$[A] = A`):
|
|
388
|
-
|
|
389
|
-
```scala
|
|
390
|
-
import zio.blocks.scope.*
|
|
391
|
-
|
|
392
|
-
val os: Scope.OpenScope = Scope.global.open()
|
|
393
|
-
|
|
394
|
-
val db = os.scope.allocate(Resource.fromAutoCloseable(new Database))
|
|
395
|
-
|
|
396
|
-
// ... use db ...
|
|
397
|
-
|
|
398
|
-
os.close().orThrow()
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
Inside a child scope, `open()` returns `$[Scope.OpenScope]`. Prefer using it safely via `$`:
|
|
402
|
-
|
|
403
|
-
```scala
|
|
404
|
-
import zio.blocks.scope.*
|
|
405
|
-
|
|
406
|
-
Scope.global.scoped { parent =>
|
|
407
|
-
import parent.*
|
|
408
|
-
|
|
409
|
-
val os: $[Scope.OpenScope] = open()
|
|
410
|
-
|
|
411
|
-
$(os) { h =>
|
|
412
|
-
val child = h.scope
|
|
413
|
-
val db = child.allocate(Resource.fromAutoCloseable(new Database))
|
|
414
|
-
|
|
415
|
-
// ...
|
|
416
|
-
h.close().orThrow()
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
---
|
|
422
|
-
|
|
423
|
-
### 8) Escape hatch: `leak`
|
|
424
|
-
|
|
425
|
-
Sometimes you must hand a raw value to code that cannot work with `$[A]`. Use `leak`:
|
|
426
|
-
|
|
427
|
-
```scala
|
|
428
|
-
import zio.blocks.scope.*
|
|
429
|
-
|
|
430
|
-
Scope.global.scoped { scope =>
|
|
431
|
-
import scope.*
|
|
432
|
-
|
|
433
|
-
val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
434
|
-
|
|
435
|
-
val raw: Database = leak(db) // emits a compiler warning
|
|
436
|
-
// thirdParty(raw)
|
|
437
|
-
}
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
`leak` bypasses compile-time guarantees—use only for unavoidable interop. If the type is genuinely pure data, prefer adding `Unscoped` so you don't need to leak.
|
|
441
|
-
|
|
442
|
-
---
|
|
443
|
-
|
|
444
|
-
## Safety model (why leaking is prevented)
|
|
445
|
-
|
|
446
|
-
Scope's safety comes from *three reinforcing layers*.
|
|
447
|
-
|
|
448
|
-
### 1) Type barrier: scope-specific `$[A]`
|
|
449
|
-
|
|
450
|
-
Every scope has a distinct `$[A]` type. You cannot accidentally use values across scopes without an explicit conversion (`lower` for parent → child).
|
|
451
|
-
|
|
452
|
-
### 2) Controlled access: `$` macro restricts lambda usage
|
|
453
|
-
|
|
454
|
-
The `$` operator only allows using the unwrapped value as a **method/field receiver**. This prevents:
|
|
455
|
-
|
|
456
|
-
- returning the resource
|
|
457
|
-
- storing it in a local val/var
|
|
458
|
-
- passing it as an argument
|
|
459
|
-
- capturing it in a closure
|
|
460
|
-
|
|
461
|
-
Also note: `$` requires a **lambda literal**. Method references / variables are rejected:
|
|
462
|
-
|
|
463
|
-
```scala
|
|
464
|
-
// does not compile:
|
|
465
|
-
val f: Database => String = _.query("x")
|
|
466
|
-
(scope $ db)(f) // "$ requires a lambda literal ..."
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### 3) Scope boundary rule: `scoped` requires `Unscoped[A]`
|
|
470
|
-
|
|
471
|
-
A `scoped { ... }` block can only return pure data (or `Nothing`). Resources and closures cannot escape.
|
|
472
|
-
|
|
473
|
-
**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`).
|
|
474
|
-
|
|
475
|
-
### Closed-scope safety (runtime)
|
|
476
|
-
|
|
477
|
-
If a scope reference escapes its `scoped { }` block and an operation is attempted after closing, Scope throws `IllegalStateException` with a detailed, actionable error message:
|
|
478
|
-
|
|
479
|
-
- **`allocate`** on a closed scope:
|
|
480
|
-
|
|
481
|
-
```
|
|
482
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
483
|
-
|
|
484
|
-
Cannot allocate resource: scope is already closed.
|
|
485
|
-
|
|
486
|
-
Scope: Scope.Child
|
|
487
|
-
|
|
488
|
-
What happened:
|
|
489
|
-
A call to allocate was made on a scope whose finalizers have
|
|
490
|
-
already run. The resource was never acquired.
|
|
491
|
-
|
|
492
|
-
Common causes:
|
|
493
|
-
• A scope reference escaped a scoped { } block (e.g. stored in a
|
|
494
|
-
field, captured in a Future or passed to another thread).
|
|
495
|
-
• close() was called on an OpenScope before all
|
|
496
|
-
allocations inside it completed.
|
|
497
|
-
|
|
498
|
-
Fix:
|
|
499
|
-
Call allocate only inside a live scoped { } block, or before
|
|
500
|
-
calling close() on an OpenScope.
|
|
501
|
-
|
|
502
|
-
// Correct usage:
|
|
503
|
-
Scope.global.scoped { scope =>
|
|
504
|
-
import scope.*
|
|
505
|
-
val db = allocate(Resource(new Database))
|
|
506
|
-
$(db)(_.query("SELECT 1"))
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
────────────────────────────────────────────────────────────────────────────────
|
|
510
|
-
```
|
|
511
|
-
|
|
512
|
-
- **`open()`** on a closed scope gives the same treatment, explaining that no child scope was created and directing the user to call `open()` only on a live scope.
|
|
513
|
-
|
|
514
|
-
- **`$`** on a closed scope explains that the resource may have already been released and accessing it would be undefined behaviour.
|
|
515
|
-
|
|
516
|
-
The following operations on a closed scope do **not** throw:
|
|
517
|
-
|
|
518
|
-
- `defer` — silently ignored (no-op)
|
|
519
|
-
- `scoped` — runs normally but creates a born-closed child scope
|
|
520
|
-
- `lower` — zero-cost cast, no closed check needed
|
|
521
|
-
|
|
522
|
-
### Thread ownership rule (JVM)
|
|
523
|
-
|
|
524
|
-
- Scopes created by `scoped` are **owned** by the entering thread.
|
|
525
|
-
- Calling `scoped` on a scope you don't own throws `IllegalStateException`.
|
|
526
|
-
- `open()` creates an **unowned** child scope (`isOwner == true` from any thread).
|
|
527
|
-
|
|
528
|
-
(Scala.js uses a trivial ownership model; `isOwner` is effectively always true.)
|
|
529
|
-
|
|
530
|
-
---
|
|
531
|
-
|
|
532
|
-
## Usage examples (patterns)
|
|
533
|
-
|
|
534
|
-
### Allocating and using a resource
|
|
535
|
-
|
|
536
|
-
```scala
|
|
537
|
-
import zio.blocks.scope.*
|
|
538
|
-
|
|
539
|
-
final class FileHandle(path: String) extends AutoCloseable:
|
|
540
|
-
def readAll(): String = s"contents of $path"
|
|
541
|
-
def close(): Unit = println(s"closed $path")
|
|
542
|
-
|
|
543
|
-
@main def fileExample(): Unit =
|
|
544
|
-
Scope.global.scoped { scope =>
|
|
545
|
-
import scope.*
|
|
546
|
-
|
|
547
|
-
val h: $[FileHandle] =
|
|
548
|
-
Resource(new FileHandle("data.txt")).allocate
|
|
549
|
-
|
|
550
|
-
val contents: String =
|
|
551
|
-
$(h)(_.readAll())
|
|
552
|
-
|
|
553
|
-
println(contents)
|
|
554
|
-
}
|
|
555
|
-
```
|
|
556
|
-
|
|
557
|
-
---
|
|
558
|
-
|
|
559
|
-
### Nested scopes (child can use parent, not vice versa)
|
|
560
|
-
|
|
561
|
-
```scala
|
|
562
|
-
import zio.blocks.scope.*
|
|
563
|
-
|
|
564
|
-
final class Database extends AutoCloseable:
|
|
565
|
-
def query(sql: String): String = s"result: $sql"
|
|
566
|
-
def close(): Unit = println("db closed")
|
|
567
|
-
|
|
568
|
-
@main def nested(): Unit =
|
|
569
|
-
Scope.global.scoped { parent =>
|
|
570
|
-
import parent.*
|
|
571
|
-
|
|
572
|
-
val parentDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
573
|
-
|
|
574
|
-
val done: String =
|
|
575
|
-
parent.scoped { child =>
|
|
576
|
-
import child.*
|
|
577
|
-
|
|
578
|
-
val db: $[Database] = lower(parentDb)
|
|
579
|
-
println($(db)(_.query("SELECT 1")))
|
|
580
|
-
|
|
581
|
-
val childDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
582
|
-
println($(childDb)(_.query("SELECT 2")))
|
|
583
|
-
|
|
584
|
-
// childDb cannot be returned to the parent (not Unscoped)
|
|
585
|
-
"done"
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
println($(parentDb)(_.query("SELECT 3")))
|
|
589
|
-
done
|
|
590
|
-
}
|
|
591
|
-
```
|
|
592
|
-
|
|
593
|
-
Finalizers run **child first, then parent**.
|
|
594
|
-
|
|
595
|
-
---
|
|
596
|
-
|
|
597
|
-
### Chaining resource acquisition (`$[Resource[A]]` + `.allocate`)
|
|
598
|
-
|
|
599
|
-
If a method returns `Resource[A]`, `$` returns a **scoped** `Resource[A]` (because `Resource[A]` is not `Unscoped`). Allocate it without leaking:
|
|
600
|
-
|
|
601
|
-
```scala
|
|
602
|
-
import zio.blocks.scope.*
|
|
603
|
-
|
|
604
|
-
final class Pool extends AutoCloseable:
|
|
605
|
-
def lease(): Resource[Conn] = Resource.fromAutoCloseable(new Conn)
|
|
606
|
-
def close(): Unit = println("pool closed")
|
|
607
|
-
|
|
608
|
-
final class Conn extends AutoCloseable:
|
|
609
|
-
def query(sql: String): String = s"result: $sql"
|
|
610
|
-
def close(): Unit = println("connection closed")
|
|
611
|
-
|
|
612
|
-
@main def chaining(): Unit =
|
|
613
|
-
Scope.global.scoped { scope =>
|
|
614
|
-
import scope.*
|
|
615
|
-
|
|
616
|
-
val pool: $[Pool] = Resource.fromAutoCloseable(new Pool).allocate
|
|
617
|
-
|
|
618
|
-
// $(pool)(_.lease()) : $[Resource[Conn]]
|
|
619
|
-
val conn: $[Conn] =
|
|
620
|
-
$(pool)(_.lease()).allocate
|
|
621
|
-
|
|
622
|
-
val result: String =
|
|
623
|
-
$(conn)(_.query("SELECT 1"))
|
|
624
|
-
|
|
625
|
-
println(result)
|
|
626
|
-
}
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
This `.allocate` comes from `Scope.ScopedResourceOps` (an extension on `$[Resource[A]]`).
|
|
630
|
-
|
|
631
|
-
---
|
|
632
|
-
|
|
633
|
-
### Allocating a bare `Resource[A]` with `.allocate`
|
|
634
|
-
|
|
635
|
-
A plain `Resource[A]` also has `.allocate` as syntax sugar for `scope.allocate(resource)`:
|
|
636
|
-
|
|
637
|
-
```scala
|
|
638
|
-
import zio.blocks.scope.*
|
|
639
|
-
|
|
640
|
-
Scope.global.scoped { scope =>
|
|
641
|
-
import scope.*
|
|
642
|
-
|
|
643
|
-
val db: $[Database] =
|
|
644
|
-
Resource.fromAutoCloseable(new Database).allocate
|
|
645
|
-
|
|
646
|
-
$(db)(_.query("SELECT 1"))
|
|
647
|
-
}
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
---
|
|
651
|
-
|
|
652
|
-
### Classes with `Finalizer` parameters (cleanup-only capability)
|
|
653
|
-
|
|
654
|
-
If a class only needs cleanup registration, accept a `Finalizer`. DI macros inject it automatically.
|
|
655
|
-
|
|
656
|
-
```scala
|
|
657
|
-
import zio.blocks.scope.*
|
|
658
|
-
|
|
659
|
-
final case class Config(url: String)
|
|
660
|
-
object Config:
|
|
661
|
-
given Unscoped[Config] = Unscoped.derived
|
|
662
|
-
|
|
663
|
-
final class ConnectionPool(config: Config)(using Finalizer):
|
|
664
|
-
private val pool = s"pool(${config.url})"
|
|
665
|
-
defer(println(s"shutdown $pool"))
|
|
666
|
-
|
|
667
|
-
val poolResource: Resource[ConnectionPool] =
|
|
668
|
-
Resource.from[ConnectionPool](
|
|
669
|
-
Wire(Config("jdbc://localhost"))
|
|
670
|
-
)
|
|
671
|
-
|
|
672
|
-
@main def finalizerInjection(): Unit =
|
|
673
|
-
Scope.global.scoped { scope =>
|
|
674
|
-
import scope.*
|
|
675
|
-
val pool: $[ConnectionPool] = poolResource.allocate
|
|
676
|
-
()
|
|
677
|
-
}
|
|
678
|
-
```
|
|
679
|
-
|
|
680
|
-
When to prefer `Finalizer` over `Scope`:
|
|
681
|
-
|
|
682
|
-
- you only need `defer`
|
|
683
|
-
- you want to expose minimal power to the class
|
|
684
|
-
|
|
685
|
-
---
|
|
686
|
-
|
|
687
|
-
### Classes with `Scope` parameters (scope injection)
|
|
688
|
-
|
|
689
|
-
If a class needs to allocate resources or create child scopes, accept a `Scope`:
|
|
690
|
-
|
|
691
|
-
```scala
|
|
692
|
-
import zio.blocks.scope.*
|
|
693
|
-
|
|
694
|
-
final case class Config(url: String)
|
|
695
|
-
object Config:
|
|
696
|
-
given Unscoped[Config] = Unscoped.derived
|
|
697
|
-
|
|
698
|
-
final class Connection(config: Config) extends AutoCloseable:
|
|
699
|
-
def query(sql: String): String = s"[${config.url}] $sql"
|
|
700
|
-
def close(): Unit = println("connection closed")
|
|
701
|
-
|
|
702
|
-
final class RequestHandler(config: Config)(using scope: Scope):
|
|
703
|
-
def handle(sql: String): String =
|
|
704
|
-
scope.scoped { child =>
|
|
705
|
-
import child.*
|
|
706
|
-
val conn: $[Connection] = Resource.fromAutoCloseable(new Connection(config)).allocate
|
|
707
|
-
$(conn)(_.query(sql))
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
val handlerResource: Resource[RequestHandler] =
|
|
711
|
-
Resource.from[RequestHandler](
|
|
712
|
-
Wire(Config("jdbc://localhost"))
|
|
713
|
-
)
|
|
714
|
-
|
|
715
|
-
@main def scopeInjection(): Unit =
|
|
716
|
-
Scope.global.scoped { scope =>
|
|
717
|
-
import scope.*
|
|
718
|
-
val handler: $[RequestHandler] = handlerResource.allocate
|
|
719
|
-
val out: String = $(handler)(_.handle("SELECT 1"))
|
|
720
|
-
println(out)
|
|
721
|
-
}
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
The `Scope`/`Finalizer` parameter can appear in any parameter list position; it's recognized specially by the derivation macros.
|
|
725
|
-
|
|
726
|
-
---
|
|
727
|
-
|
|
728
|
-
## Dependency injection (DI) with `Wire` + `Resource.from`
|
|
729
|
-
|
|
730
|
-
Scope includes a small constructor-based DI layer built on top of `zio.blocks.context.Context`. For a comprehensive guide to `Wire` and its construction patterns, see the [Wire reference](./wire.md) page.
|
|
731
|
-
|
|
732
|
-
### `Wire[-In, +Out]`: a dependency recipe
|
|
733
|
-
|
|
734
|
-
A `Wire` is a recipe for constructing `Out` from a `Context[In]` (and a `Scope` for finalization):
|
|
735
|
-
|
|
736
|
-
- `Wire.Shared` → converts to `Resource.shared` (ref-counted sharing)
|
|
737
|
-
- `Wire.Unique` → converts to `Resource.unique` (fresh instance)
|
|
738
|
-
|
|
739
|
-
#### Manual wire + Context
|
|
740
|
-
|
|
741
|
-
```scala
|
|
742
|
-
import zio.blocks.scope.*
|
|
743
|
-
import zio.blocks.context.Context
|
|
744
|
-
|
|
745
|
-
final case class Config(debug: Boolean)
|
|
746
|
-
object Config:
|
|
747
|
-
given Unscoped[Config] = Unscoped.derived
|
|
748
|
-
|
|
749
|
-
val w: Wire.Shared[Boolean, Config] =
|
|
750
|
-
Wire.shared[Config] // Boolean => Config
|
|
751
|
-
|
|
752
|
-
val deps: Context[Boolean] =
|
|
753
|
-
Context(true)
|
|
754
|
-
|
|
755
|
-
@main def wireAndContext(): Unit =
|
|
756
|
-
Scope.global.scoped { scope =>
|
|
757
|
-
import scope.*
|
|
758
|
-
|
|
759
|
-
val cfg: $[Config] =
|
|
760
|
-
allocate(w.toResource(deps))
|
|
761
|
-
|
|
762
|
-
val debug: Boolean =
|
|
763
|
-
$(cfg)(_.debug)
|
|
764
|
-
|
|
765
|
-
println(debug)
|
|
766
|
-
}
|
|
767
|
-
```
|
|
768
|
-
|
|
769
|
-
#### Sharing vs uniqueness at the wire level
|
|
770
|
-
|
|
771
|
-
```scala
|
|
772
|
-
import zio.blocks.scope.*
|
|
773
|
-
|
|
774
|
-
val ws = Wire.shared[Config] // shared recipe
|
|
775
|
-
val wu = Wire.unique[Config] // unique recipe
|
|
776
|
-
```
|
|
777
|
-
|
|
778
|
-
The difference is realized when converting to resources (`toResource`) and allocating.
|
|
779
|
-
|
|
780
|
-
---
|
|
781
|
-
|
|
782
|
-
### `Resource.from[T](wires*)`: derive a whole object graph
|
|
783
|
-
|
|
784
|
-
`Resource.from[T](wires*)` is the primary entry point for DI. It:
|
|
785
|
-
|
|
786
|
-
- uses provided wires as overrides
|
|
787
|
-
- auto-creates missing wires for **concrete classes** (defaulting to shared)
|
|
788
|
-
- rejects unmakeable/abstract types unless you provide a wire
|
|
789
|
-
- detects cycles, duplicate providers, and subtype conflicts
|
|
790
|
-
- generates a composed `Resource[T]` via `flatMap` chains (preserving sharing/uniqueness)
|
|
791
|
-
|
|
792
|
-
Example:
|
|
793
|
-
|
|
794
|
-
```scala
|
|
795
|
-
import zio.blocks.scope.*
|
|
796
|
-
|
|
797
|
-
final case class Config(url: String)
|
|
798
|
-
object Config:
|
|
799
|
-
given Unscoped[Config] = Unscoped.derived
|
|
800
|
-
|
|
801
|
-
final class Logger:
|
|
802
|
-
def info(msg: String): Unit = println(msg)
|
|
803
|
-
|
|
804
|
-
final class Database(cfg: Config) extends AutoCloseable:
|
|
805
|
-
def query(sql: String): String = s"[${cfg.url}] $sql"
|
|
806
|
-
def close(): Unit = println("database closed")
|
|
807
|
-
|
|
808
|
-
final class Service(db: Database, logger: Logger) extends AutoCloseable:
|
|
809
|
-
def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
|
|
810
|
-
def close(): Unit = println("service closed")
|
|
811
|
-
|
|
812
|
-
val serviceResource: Resource[Service] =
|
|
813
|
-
Resource.from[Service](
|
|
814
|
-
Wire(Config("jdbc:postgresql://localhost/db")) // leaf value
|
|
815
|
-
)
|
|
816
|
-
|
|
817
|
-
@main def di(): Unit =
|
|
818
|
-
Scope.global.scoped { scope =>
|
|
819
|
-
import scope.*
|
|
820
|
-
val svc: $[Service] = serviceResource.allocate
|
|
821
|
-
$(svc)(_.run())
|
|
822
|
-
}
|
|
823
|
-
```
|
|
824
|
-
|
|
825
|
-
#### Injecting traits via subtype wires
|
|
826
|
-
|
|
827
|
-
When a dependency is abstract, provide a wire for a concrete implementation:
|
|
828
|
-
|
|
829
|
-
```scala
|
|
830
|
-
import zio.blocks.scope.*
|
|
831
|
-
|
|
832
|
-
trait Logger:
|
|
833
|
-
def info(msg: String): Unit
|
|
834
|
-
|
|
835
|
-
final class ConsoleLogger extends Logger:
|
|
836
|
-
def info(msg: String): Unit = println(msg)
|
|
837
|
-
|
|
838
|
-
final class App(logger: Logger):
|
|
839
|
-
def run(): Unit = logger.info("Hello!")
|
|
840
|
-
|
|
841
|
-
val appResource: Resource[App] =
|
|
842
|
-
Resource.from[App](
|
|
843
|
-
Wire.shared[ConsoleLogger] // satisfies Logger via subtyping
|
|
844
|
-
)
|
|
845
|
-
|
|
846
|
-
@main def traitInjection(): Unit =
|
|
847
|
-
Scope.global.scoped { scope =>
|
|
848
|
-
import scope.*
|
|
849
|
-
val app: $[App] = appResource.allocate
|
|
850
|
-
$(app)(_.run())
|
|
851
|
-
}
|
|
852
|
-
```
|
|
853
|
-
|
|
854
|
-
#### Diamond patterns share a single instance (when appropriate)
|
|
855
|
-
|
|
856
|
-
```scala
|
|
857
|
-
import zio.blocks.scope.*
|
|
858
|
-
|
|
859
|
-
trait Service
|
|
860
|
-
final class LiveService extends Service
|
|
861
|
-
final class NeedsService(s: Service)
|
|
862
|
-
final class NeedsLive(l: LiveService)
|
|
863
|
-
final class App(a: NeedsService, b: NeedsLive)
|
|
864
|
-
|
|
865
|
-
val appResource: Resource[App] =
|
|
866
|
-
Resource.from[App](
|
|
867
|
-
Wire.shared[LiveService]
|
|
868
|
-
)
|
|
869
|
-
// LiveService instantiations: 1
|
|
870
|
-
```
|
|
871
|
-
|
|
872
|
-
---
|
|
873
|
-
|
|
874
|
-
## Common runtime errors (and what they mean)
|
|
875
|
-
|
|
876
|
-
These `IllegalStateException`s are thrown when a scope operation is attempted on a closed scope. Each message identifies the scope type, explains what went wrong, lists common causes, and shows a correct usage example.
|
|
877
|
-
|
|
878
|
-
### `allocate` on a closed scope
|
|
879
|
-
|
|
880
|
-
```
|
|
881
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
882
|
-
|
|
883
|
-
Cannot allocate resource: scope is already closed.
|
|
884
|
-
|
|
885
|
-
Scope: Scope.Child
|
|
886
|
-
|
|
887
|
-
What happened:
|
|
888
|
-
A call to allocate was made on a scope whose finalizers have
|
|
889
|
-
already run. The resource was never acquired.
|
|
890
|
-
|
|
891
|
-
Common causes:
|
|
892
|
-
• A scope reference escaped a scoped { } block (e.g. stored in a
|
|
893
|
-
field, captured in a Future or passed to another thread).
|
|
894
|
-
• close() was called on an OpenScope before all
|
|
895
|
-
allocations inside it completed.
|
|
896
|
-
|
|
897
|
-
Fix:
|
|
898
|
-
Call allocate only inside a live scoped { } block, or before
|
|
899
|
-
calling close() on an OpenScope.
|
|
900
|
-
|
|
901
|
-
// Correct usage:
|
|
902
|
-
Scope.global.scoped { scope =>
|
|
903
|
-
import scope.*
|
|
904
|
-
val db = allocate(Resource(new Database))
|
|
905
|
-
$(db)(_.query("SELECT 1"))
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
────────────────────────────────────────────────────────────────────────────────
|
|
909
|
-
```
|
|
910
|
-
|
|
911
|
-
### `open()` on a closed scope
|
|
912
|
-
|
|
913
|
-
```
|
|
914
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
915
|
-
|
|
916
|
-
Cannot open child scope: scope is already closed.
|
|
917
|
-
|
|
918
|
-
Scope: Scope.Child
|
|
919
|
-
|
|
920
|
-
What happened:
|
|
921
|
-
A call to open() was made on a scope whose finalizers have
|
|
922
|
-
already run. No child scope was created.
|
|
923
|
-
|
|
924
|
-
Common causes:
|
|
925
|
-
• A scope reference escaped a scoped { } block and open()
|
|
926
|
-
was called after the block exited.
|
|
927
|
-
• close() was called on the parent OpenScope before
|
|
928
|
-
open() was called on it.
|
|
929
|
-
|
|
930
|
-
Fix:
|
|
931
|
-
Call open() only on a live (not yet closed) scope.
|
|
932
|
-
|
|
933
|
-
// Correct usage:
|
|
934
|
-
Scope.global.scoped { scope =>
|
|
935
|
-
import scope.*
|
|
936
|
-
val child = open()
|
|
937
|
-
$(child)(_.scope.allocate(Resource(new Database)))
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
────────────────────────────────────────────────────────────────────────────────
|
|
941
|
-
```
|
|
942
|
-
|
|
943
|
-
### `$` on a closed scope
|
|
944
|
-
|
|
945
|
-
```
|
|
946
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
947
|
-
|
|
948
|
-
Cannot access scoped value: scope is already closed.
|
|
949
|
-
|
|
950
|
-
Scope: Scope.Child
|
|
951
|
-
|
|
952
|
-
What happened:
|
|
953
|
-
The $ operator was called on a scope whose finalizers have
|
|
954
|
-
already run. The underlying resource may have been released.
|
|
955
|
-
Accessing it would be undefined behavior.
|
|
956
|
-
|
|
957
|
-
Common causes:
|
|
958
|
-
• A $[A] value or its owning scope escaped a scoped { }
|
|
959
|
-
block (e.g. captured in a Future, stored in a field, or
|
|
960
|
-
passed to another thread).
|
|
961
|
-
• close() was called on an OpenScope that still has
|
|
962
|
-
live $[A] values being accessed.
|
|
963
|
-
|
|
964
|
-
Fix:
|
|
965
|
-
Ensure all $ calls occur strictly within the scoped { }
|
|
966
|
-
block that owns the value, and that the scope has not been closed.
|
|
967
|
-
|
|
968
|
-
// Correct usage:
|
|
969
|
-
Scope.global.scoped { scope =>
|
|
970
|
-
import scope.*
|
|
971
|
-
val db = allocate(Resource(new Database))
|
|
972
|
-
$(db)(_.query("SELECT 1")) // $ used inside the block
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
────────────────────────────────────────────────────────────────────────────────
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
---
|
|
979
|
-
|
|
980
|
-
## Common compile errors (and what they mean)
|
|
981
|
-
|
|
982
|
-
This module produces two kinds of compile-time feedback:
|
|
983
|
-
|
|
984
|
-
- **Plain macro aborts** for unsafe `$` usage
|
|
985
|
-
- **ASCII-rendered** errors/warnings for DI derivation + leak warnings (via `internal.ErrorMessages`)
|
|
986
|
-
|
|
987
|
-
### Unsafe use inside `$`
|
|
988
|
-
|
|
989
|
-
All messages name the offending parameter by its 1-based index and source name, and end with the receiver-only reminder. Typical messages:
|
|
990
|
-
|
|
991
|
-
```
|
|
992
|
-
Parameter 1 ('d') cannot be passed as an argument to a function or method.
|
|
993
|
-
Scoped values may only be used as a method receiver (e.g., d.method()).
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
```
|
|
997
|
-
Parameter 1 ('d') must only be used as a method receiver.
|
|
998
|
-
It cannot be returned, stored, passed as an argument, or captured.
|
|
999
|
-
Scoped values may only be used as a method receiver (e.g., d.method()).
|
|
1000
|
-
```
|
|
1001
|
-
|
|
1002
|
-
```
|
|
1003
|
-
Parameter 1 ('d') cannot be captured in a nested lambda, def, or anonymous class.
|
|
1004
|
-
Scoped values may only be used as a method receiver (e.g., d.method()).
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
```
|
|
1008
|
-
Parameter 2 ('cache') cannot be passed as an argument to a function or method.
|
|
1009
|
-
Scoped values may only be used as a method receiver (e.g., cache.method()).
|
|
1010
|
-
```
|
|
1011
|
-
|
|
1012
|
-
```
|
|
1013
|
-
$ requires a lambda literal, e.g. $(x)(a => a.method()).
|
|
1014
|
-
Method references and variables are not supported.
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
### Not a class (`Wire.shared/unique` on a trait / abstract)
|
|
1018
|
-
|
|
1019
|
-
```
|
|
1020
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
1021
|
-
|
|
1022
|
-
Cannot derive Wire for MyTrait: not a class.
|
|
1023
|
-
|
|
1024
|
-
Hint: Use Wire.Shared / Wire.Unique directly.
|
|
1025
|
-
|
|
1026
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1027
|
-
```
|
|
1028
|
-
|
|
1029
|
-
### No primary constructor
|
|
1030
|
-
|
|
1031
|
-
```
|
|
1032
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
1033
|
-
|
|
1034
|
-
MyType has no primary constructor.
|
|
1035
|
-
|
|
1036
|
-
Hint: Use Wire.Shared / Wire.Unique directly
|
|
1037
|
-
with a custom construction strategy.
|
|
1038
|
-
|
|
1039
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1040
|
-
```
|
|
1041
|
-
|
|
1042
|
-
### `Resource.from[T]` used when `T` has dependencies
|
|
1043
|
-
|
|
1044
|
-
```
|
|
1045
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
1046
|
-
|
|
1047
|
-
Resource.from[MyService] cannot be derived.
|
|
1048
|
-
|
|
1049
|
-
MyService has dependencies that must be provided:
|
|
1050
|
-
• Config
|
|
1051
|
-
• Logger
|
|
1052
|
-
|
|
1053
|
-
Hint: Use Resource.from[MyService](wire1, wire2, ...)
|
|
1054
|
-
to provide wires for all dependencies.
|
|
1055
|
-
|
|
1056
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1057
|
-
```
|
|
1058
|
-
|
|
1059
|
-
### Unmakeable type (primitives, functions, collections)
|
|
1060
|
-
|
|
1061
|
-
```
|
|
1062
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
1063
|
-
|
|
1064
|
-
Cannot auto-create String
|
|
1065
|
-
|
|
1066
|
-
This type (primitive, collection, or function) cannot be auto-created.
|
|
1067
|
-
|
|
1068
|
-
Required by:
|
|
1069
|
-
├── Config
|
|
1070
|
-
└── App
|
|
1071
|
-
|
|
1072
|
-
Fix: Provide Wire(value) with the desired value:
|
|
1073
|
-
|
|
1074
|
-
Resource.from[...](
|
|
1075
|
-
Wire(...), // provide a value for String
|
|
1076
|
-
...
|
|
1077
|
-
)
|
|
1078
|
-
|
|
1079
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1080
|
-
```
|
|
1081
|
-
|
|
1082
|
-
### Abstract type (trait / abstract class dependency)
|
|
1083
|
-
|
|
1084
|
-
```
|
|
1085
|
-
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
1086
|
-
|
|
1087
|
-
Cannot auto-create Logger
|
|
1088
|
-
|
|
1089
|
-
This type is abstract (trait or abstract class).
|
|
1090
|
-
|
|
1091
|
-
Required by:
|
|
1092
|
-
└── App
|
|
1093
|
-
|
|
1094
|
-
Fix: Provide a wire for a concrete implementation:
|
|
1095
|
-
|
|
1096
|
-
Resource.from[...](
|
|
1097
|
-
Wire.shared[ConcreteImpl], // provides Logger
|
|
1098
|
-
...
|
|
1099
|
-
)
|
|
1100
|
-
|
|
1101
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1102
|
-
```
|
|
1103
|
-
|
|
1104
|
-
### Duplicate providers (ambiguous wires)
|
|
1105
|
-
|
|
1106
|
-
```
|
|
1107
|
-
── Scope Error ────────────────────────────────────────────────────────────────
|
|
1108
|
-
|
|
1109
|
-
Multiple providers for Service
|
|
1110
|
-
|
|
1111
|
-
Conflicting wires:
|
|
1112
|
-
1. LiveService
|
|
1113
|
-
2. TestService
|
|
1114
|
-
|
|
1115
|
-
Hint: Remove duplicate wires or use distinct wrapper types.
|
|
1116
|
-
|
|
1117
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1118
|
-
```
|
|
1119
|
-
|
|
1120
|
-
### Dependency cycle
|
|
1121
|
-
|
|
1122
|
-
```
|
|
1123
|
-
── Scope Error ────────────────────────────────────────────────────────────────
|
|
1124
|
-
|
|
1125
|
-
Dependency cycle detected
|
|
1126
|
-
|
|
1127
|
-
Cycle:
|
|
1128
|
-
┌───────────┐
|
|
1129
|
-
│ ▼
|
|
1130
|
-
A ──► B ──► C
|
|
1131
|
-
▲ │
|
|
1132
|
-
└───────────┘
|
|
1133
|
-
|
|
1134
|
-
Break the cycle by:
|
|
1135
|
-
• Introducing an interface/trait
|
|
1136
|
-
• Using lazy initialization
|
|
1137
|
-
• Restructuring dependencies
|
|
1138
|
-
|
|
1139
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1140
|
-
```
|
|
1141
|
-
|
|
1142
|
-
### Subtype conflict (related dependency types)
|
|
1143
|
-
|
|
1144
|
-
```
|
|
1145
|
-
── Scope Error ────────────────────────────────────────────────────────────────
|
|
1146
|
-
|
|
1147
|
-
Dependency type conflict in MyService
|
|
1148
|
-
|
|
1149
|
-
FileInputStream is a subtype of InputStream.
|
|
1150
|
-
|
|
1151
|
-
When both types are dependencies, Context cannot reliably distinguish
|
|
1152
|
-
them. The more specific type may be retrieved when the more general
|
|
1153
|
-
type is requested.
|
|
1154
|
-
|
|
1155
|
-
To fix this, wrap one or both types in a distinct wrapper:
|
|
1156
|
-
|
|
1157
|
-
case class WrappedInputStream(value: InputStream)
|
|
1158
|
-
or
|
|
1159
|
-
opaque type WrappedInputStream = InputStream
|
|
1160
|
-
|
|
1161
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
### Duplicate parameter types in a constructor
|
|
1165
|
-
|
|
1166
|
-
```
|
|
1167
|
-
── Scope Error ────────────────────────────────────────────────────────────────
|
|
1168
|
-
|
|
1169
|
-
Constructor of App has multiple parameters of type String
|
|
1170
|
-
|
|
1171
|
-
Context is type-indexed and cannot supply distinct values for the same type.
|
|
1172
|
-
|
|
1173
|
-
Fix: Wrap one parameter in an opaque type to distinguish them:
|
|
1174
|
-
|
|
1175
|
-
opaque type FirstString = String
|
|
1176
|
-
or
|
|
1177
|
-
case class FirstString(value: String)
|
|
1178
|
-
|
|
1179
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1180
|
-
```
|
|
1181
|
-
|
|
1182
|
-
### Leak warning
|
|
1183
|
-
|
|
1184
|
-
```
|
|
1185
|
-
── Scope Warning ───────────────────────────────────────────────────────────────
|
|
1186
|
-
|
|
1187
|
-
leak(db)
|
|
1188
|
-
^
|
|
1189
|
-
|
|
|
1190
|
-
|
|
1191
|
-
Warning: db is being leaked from scope zio.blocks.scope.Scope.Child[...].
|
|
1192
|
-
This may result in undefined behavior.
|
|
1193
|
-
|
|
1194
|
-
Hint:
|
|
1195
|
-
If you know this data type is not resourceful, then add an Unscoped
|
|
1196
|
-
instance for it so you do not need to leak it.
|
|
1197
|
-
|
|
1198
|
-
───────────────────────────────────────────────────────────────────────────────
|
|
1199
|
-
```
|
|
1200
|
-
|
|
1201
|
-
---
|
|
1202
|
-
|
|
1203
|
-
## API reference (from source)
|
|
1204
|
-
|
|
1205
|
-
Examples below use Scala 3 syntax. Scala 2.13 has equivalent APIs, but macro signatures differ slightly (notably `$`'s return type encoding).
|
|
1206
|
-
|
|
1207
|
-
### `Scope`
|
|
1208
|
-
|
|
1209
|
-
```scala
|
|
1210
|
-
sealed abstract class Scope extends Finalizer with ScopeVersionSpecific
|
|
1211
|
-
```
|
|
1212
|
-
|
|
1213
|
-
Associated types and hierarchy:
|
|
1214
|
-
|
|
1215
|
-
- `type $[+A]`
|
|
1216
|
-
- `type Parent <: Scope`
|
|
1217
|
-
- `val parent: Parent`
|
|
1218
|
-
- `def isClosed: Boolean`
|
|
1219
|
-
- `def isOwner: Boolean`
|
|
1220
|
-
|
|
1221
|
-
Core operations:
|
|
1222
|
-
|
|
1223
|
-
```scala
|
|
1224
|
-
def scoped[A](f: (child: Scope.Child[this.type]) => A)(using Unscoped[A]): A
|
|
1225
|
-
|
|
1226
|
-
def allocate[A](resource: Resource[A]): $[A]
|
|
1227
|
-
def allocate[A <: AutoCloseable](value: => A): $[A]
|
|
1228
|
-
|
|
1229
|
-
// N=1 (infix available: `scope $ sa`)
|
|
1230
|
-
infix transparent inline def $[A, B](sa: $[A])(inline f: A => B): B | $[B]
|
|
1231
|
-
|
|
1232
|
-
// N=2..5 (unqualified syntax: `$(sa1, sa2)(f)` after `import scope.*`)
|
|
1233
|
-
transparent inline def $[A1, A2, B](sa1: $[A1], sa2: $[A2])(inline f: (A1, A2) => B): B | $[B]
|
|
1234
|
-
transparent inline def $[A1, A2, A3, B](sa1: $[A1], sa2: $[A2], sa3: $[A3])(inline f: (A1, A2, A3) => B): B | $[B]
|
|
1235
|
-
transparent inline def $[A1, A2, A3, A4, B](sa1: $[A1], sa2: $[A2], sa3: $[A3], sa4: $[A4])(inline f: (A1, A2, A3, A4) => B): B | $[B]
|
|
1236
|
-
transparent inline def $[A1, A2, A3, A4, A5, B](sa1: $[A1], sa2: $[A2], sa3: $[A3], sa4: $[A4], sa5: $[A5])(inline f: (A1, A2, A3, A4, A5) => B): B | $[B]
|
|
1237
|
-
|
|
1238
|
-
def lower[A](value: parent.$[A]): $[A]
|
|
1239
|
-
|
|
1240
|
-
override def defer(f: => Unit): DeferHandle
|
|
1241
|
-
|
|
1242
|
-
def open(): $[Scope.OpenScope]
|
|
1243
|
-
|
|
1244
|
-
inline def leak[A](inline sa: $[A]): A
|
|
1245
|
-
```
|
|
1246
|
-
|
|
1247
|
-
Notes:
|
|
1248
|
-
|
|
1249
|
-
- `$` (all arities) requires a **lambda literal** and enforces safe receiver-only usage at compile time.
|
|
1250
|
-
- `$` returns `B` if `Unscoped[B]` exists; otherwise returns `$[B]`.
|
|
1251
|
-
- N=1 is `infix`; N≥2 are not — use unqualified syntax after `import scope.*`.
|
|
1252
|
-
- For N>5, call `$` once per resource and combine the resulting plain (Unscoped) values.
|
|
1253
|
-
- If the scope is closed, `$`, `allocate`, and `open` throw `IllegalStateException` with a detailed error message. `defer` and `lower` are unaffected.
|
|
1254
|
-
|
|
1255
|
-
Syntax enrichments available after `import scope.*` inside a scope:
|
|
1256
|
-
|
|
1257
|
-
```scala
|
|
1258
|
-
implicit class ScopedResourceOps[A](sr: $[Resource[A]]):
|
|
1259
|
-
def allocate: $[A]
|
|
1260
|
-
|
|
1261
|
-
implicit class ResourceOps[A](r: Resource[A]):
|
|
1262
|
-
def allocate: $[A]
|
|
1263
|
-
```
|
|
1264
|
-
|
|
1265
|
-
---
|
|
1266
|
-
|
|
1267
|
-
### `Scope.global`
|
|
1268
|
-
|
|
1269
|
-
```scala
|
|
1270
|
-
object Scope:
|
|
1271
|
-
object global extends Scope
|
|
1272
|
-
```
|
|
1273
|
-
|
|
1274
|
-
Properties:
|
|
1275
|
-
|
|
1276
|
-
- `type $[+A] = A` (identity)
|
|
1277
|
-
- `isOwner` always returns `true`
|
|
1278
|
-
- JVM: finalizers run at shutdown via a shutdown hook
|
|
1279
|
-
- Scala.js: shutdown hook is not available
|
|
1280
|
-
|
|
1281
|
-
---
|
|
1282
|
-
|
|
1283
|
-
### `Scope.OpenScope`
|
|
1284
|
-
|
|
1285
|
-
```scala
|
|
1286
|
-
case class OpenScope(scope: Scope, close: () => Finalization)
|
|
1287
|
-
```
|
|
1288
|
-
|
|
1289
|
-
- `scope`: the child scope
|
|
1290
|
-
- `close()`: detaches from parent, runs child finalizers (LIFO), returns `Finalization`
|
|
1291
|
-
|
|
1292
|
-
---
|
|
1293
|
-
|
|
1294
|
-
### `Finalizer`
|
|
1295
|
-
|
|
1296
|
-
```scala
|
|
1297
|
-
trait Finalizer:
|
|
1298
|
-
def defer(f: => Unit): DeferHandle
|
|
1299
|
-
```
|
|
1300
|
-
|
|
1301
|
-
A minimal capability interface for registering cleanup.
|
|
1302
|
-
|
|
1303
|
-
Also available as a package-level helper:
|
|
1304
|
-
|
|
1305
|
-
```scala
|
|
1306
|
-
def defer(finalizer: => Unit)(using fin: Finalizer): DeferHandle
|
|
1307
|
-
```
|
|
1308
|
-
|
|
1309
|
-
---
|
|
1310
|
-
|
|
1311
|
-
### `DeferHandle`
|
|
1312
|
-
|
|
1313
|
-
```scala
|
|
1314
|
-
abstract class DeferHandle:
|
|
1315
|
-
def cancel(): Unit
|
|
1316
|
-
```
|
|
1317
|
-
|
|
1318
|
-
- `cancel()` is thread-safe and idempotent
|
|
1319
|
-
- cancellation is O(1) (true removal from a concurrent map)
|
|
1320
|
-
|
|
1321
|
-
---
|
|
1322
|
-
|
|
1323
|
-
### `Finalization`
|
|
1324
|
-
|
|
1325
|
-
```scala
|
|
1326
|
-
final class Finalization(val errors: zio.blocks.chunk.Chunk[Throwable]):
|
|
1327
|
-
def isEmpty: Boolean
|
|
1328
|
-
def nonEmpty: Boolean
|
|
1329
|
-
def orThrow(): Unit
|
|
1330
|
-
def suppress(initial: Throwable): Throwable
|
|
1331
|
-
|
|
1332
|
-
object Finalization:
|
|
1333
|
-
val empty: Finalization
|
|
1334
|
-
def apply(errors: Chunk[Throwable]): Finalization
|
|
1335
|
-
```
|
|
1336
|
-
|
|
1337
|
-
---
|
|
1338
|
-
|
|
1339
|
-
### `Resource[+A]`
|
|
1340
|
-
|
|
1341
|
-
```scala
|
|
1342
|
-
sealed trait Resource[+A]:
|
|
1343
|
-
def map[B](f: A => B): Resource[B]
|
|
1344
|
-
def flatMap[B](f: A => Resource[B]): Resource[B]
|
|
1345
|
-
def zip[B](that: Resource[B]): Resource[(A, B)]
|
|
1346
|
-
```
|
|
1347
|
-
|
|
1348
|
-
Companion constructors:
|
|
1349
|
-
|
|
1350
|
-
```scala
|
|
1351
|
-
object Resource:
|
|
1352
|
-
def apply[A](value: => A): Resource[A]
|
|
1353
|
-
def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
|
|
1354
|
-
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
|
|
1355
|
-
def shared[A](f: Scope => A): Resource[A]
|
|
1356
|
-
def unique[A](f: Scope => A): Resource[A]
|
|
1357
|
-
|
|
1358
|
-
inline def from[T]: Resource[T]
|
|
1359
|
-
inline def from[T](inline wires: Wire[?, ?]*): Resource[T]
|
|
1360
|
-
```
|
|
1361
|
-
|
|
1362
|
-
Notes:
|
|
1363
|
-
|
|
1364
|
-
- `Resource.from[T]` (no args) only works when `T` has **no non-scope dependencies** (constructor params may include `Scope`/`Finalizer`).
|
|
1365
|
-
- Use `Resource.from[T](wires*)` to provide/override dependencies and derive the full graph.
|
|
1366
|
-
|
|
1367
|
-
---
|
|
1368
|
-
|
|
1369
|
-
### `Wire[-In, +Out]`
|
|
1370
|
-
|
|
1371
|
-
```scala
|
|
1372
|
-
sealed trait Wire[-In, +Out]:
|
|
1373
|
-
def isShared: Boolean
|
|
1374
|
-
def isUnique: Boolean = !isShared
|
|
1375
|
-
|
|
1376
|
-
def shared: Wire.Shared[In, Out]
|
|
1377
|
-
def unique: Wire.Unique[In, Out]
|
|
1378
|
-
|
|
1379
|
-
def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
|
|
1380
|
-
```
|
|
1381
|
-
|
|
1382
|
-
Wires:
|
|
1383
|
-
|
|
1384
|
-
```scala
|
|
1385
|
-
object Wire:
|
|
1386
|
-
final case class Shared[-In, +Out](makeFn: (Scope, Context[In]) => Out) extends Wire[In, Out]
|
|
1387
|
-
final case class Unique[-In, +Out](makeFn: (Scope, Context[In]) => Out) extends Wire[In, Out]
|
|
1388
|
-
|
|
1389
|
-
def apply[T](t: T): Wire.Shared[Any, T]
|
|
1390
|
-
|
|
1391
|
-
transparent inline def shared[T]: Wire.Shared[?, T]
|
|
1392
|
-
transparent inline def unique[T]: Wire.Unique[?, T]
|
|
1393
|
-
```
|
|
1394
|
-
|
|
1395
|
-
Notes:
|
|
1396
|
-
|
|
1397
|
-
- `Wire(t)` wraps a pre-existing value; if it's `AutoCloseable`, `close()` is registered automatically when used.
|
|
1398
|
-
|
|
1399
|
-
---
|
|
1400
|
-
|
|
1401
|
-
### `Unscoped[A]`
|
|
1402
|
-
|
|
1403
|
-
```scala
|
|
1404
|
-
trait Unscoped[A]
|
|
1405
|
-
|
|
1406
|
-
object Unscoped:
|
|
1407
|
-
inline given derived[A](using scala.deriving.Mirror.Of[A]): Unscoped[A]
|
|
1408
|
-
// plus many built-in givens (primitives, collections, time, UUID, Chunk, ...)
|
|
1409
|
-
```
|
|
1410
|
-
|
|
1411
|
-
---
|
|
1412
|
-
|
|
1413
|
-
## Practical guidance (summary)
|
|
1414
|
-
|
|
1415
|
-
- Allocate in a scope: `resource.allocate` (inside `Scope.global.scoped { scope => import scope.* ... }`)
|
|
1416
|
-
- Access one scoped value: `$(sa)(v => v.method())` — parameter can only be a receiver
|
|
1417
|
-
- Access two or more scoped values simultaneously: `$(sa1, sa2)((v1, v2) => v1.method(v2.result()))` (N=2..5)
|
|
1418
|
-
- For N>5: call `$` once per resource, combine the plain results
|
|
1419
|
-
- Return only `Unscoped` data from `scoped` blocks
|
|
1420
|
-
- Use `lower` to use parent values inside a child
|
|
1421
|
-
- If `$` returns `$[Resource[A]]`, call `.allocate` on it (scoped resource chaining)
|
|
1422
|
-
- Use `open()` for explicitly-managed, cross-thread capable scopes
|
|
1423
|
-
- Use `leak` only when interop forces it; prefer `Unscoped` for pure data
|