@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.
@@ -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