@zio.dev/zio-blocks 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/scope.md CHANGED
@@ -7,8 +7,9 @@
7
7
  If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the same problem space, but it focuses on:
8
8
 
9
9
  - **Compile-time prevention of scope leaks**
10
- - **Unified scoped type** (`A @@ S` is a type alias for `Scoped[A, S]`)
10
+ - **Zero-cost opaque type** (`$[A]` is the scoped type, equal to `A` at runtime)
11
11
  - **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
12
+ - **Eager evaluation** (all operations execute immediately, no deferred thunks)
12
13
 
13
14
  ---
14
15
 
@@ -16,19 +17,19 @@ If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the
16
17
 
17
18
  - [Quick start](#quick-start)
18
19
  - [Core concepts](#core-concepts)
19
- - [1) `Scope[ParentTag, Tag]`](#1-scopeparenttag-tag)
20
- - [2) Scoped values: `A @@ S`](#2-scoped-values-a--s)
20
+ - [1) `Scope`](#1-scope)
21
+ - [2) Scoped values: `$[+A]`](#2-scoped-values-a)
21
22
  - [3) `Resource[A]`: acquisition + finalization](#3-resourcea-acquisition--finalization)
22
- - [4) `A @@ S`: unified scoped type](#4-a--s-unified-scoped-type)
23
- - [5) `Unscoped`: marking pure data types](#5-unscoped-marking-pure-data-types)
24
- - [6) `ScopeLift[A, S]`: what may return from child scopes](#6-scopelifta-s-what-may-return-from-child-scopes)
25
- - [7) `Wire[-In, +Out]`: dependency recipes](#7-wire-in-out-dependency-recipes)
23
+ - [4) `Unscoped`: marking pure data types](#4-unscoped-marking-pure-data-types)
24
+ - [5) `lower`: accessing parent-scoped values](#5-lower-accessing-parent-scoped-values)
25
+ - [6) `Wire[-In, +Out]`: dependency recipes](#6-wire-in-out-dependency-recipes)
26
26
  - [Safety model (why leaking is prevented)](#safety-model-why-leaking-is-prevented)
27
27
  - [Usage examples](#usage-examples)
28
28
  - [Allocating and using a resource](#allocating-and-using-a-resource)
29
29
  - [Nested scopes (child can use parent, not vice versa)](#nested-scopes-child-can-use-parent-not-vice-versa)
30
- - [Building a `Scoped` program (map/flatMap)](#building-a-scoped-program-mapflatmap)
30
+ - [Chaining resource acquisition](#chaining-resource-acquisition)
31
31
  - [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
32
+ - [Classes with `Finalizer` parameters](#classes-with-finalizer-parameters)
32
33
  - [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
33
34
  - [Dependency injection with `Resource.from[T](wires*)`](#dependency-injection-with-resourcefromtwires)
34
35
  - [Injecting traits via subtype wires](#injecting-traits-via-subtype-wires)
@@ -49,91 +50,112 @@ final class Database extends AutoCloseable {
49
50
  }
50
51
 
51
52
  Scope.global.scoped { scope =>
52
- val db: Database @@ scope.Tag =
53
- scope.allocate(Resource(new Database))
54
-
55
- // $ executes immediately and returns String @@ scope.Tag
56
- // Use the function to work with the value
57
- (scope $ db) { database =>
58
- val result = database.query("SELECT 1")
59
- println(result)
60
- }
53
+ import scope._
54
+
55
+ val db: $[Database] = allocate(Resource(new Database))
56
+
57
+ // scope.use applies a function to the scoped value, returning a scoped result
58
+ val result: $[String] = scope.use(db)(_.query("SELECT 1"))
59
+ println(result) // $[String] = String at runtime, prints directly
61
60
  }
62
61
  ```
63
62
 
64
63
  Key things to notice:
65
64
 
66
- - `scope.allocate(...)` returns a **scoped** value: `Database @@ scope.Tag`
67
- - You **cannot** call `db.query(...)` directly (methods are intentionally hidden)
68
- - You must use `(scope $ db) { ... }` to access the value - the function executes immediately
69
- - `$` and `execute` always return scoped values (`B @@ scope.Tag`), never raw values
65
+ - `allocate(...)` returns a **scoped** value of type `$[Database]` (the path-dependent type of the enclosing scope)
66
+ - `$[A] = A` at runtime zero-cost opaque type, no boxing
67
+ - All operations are **eager** values are computed immediately, no lazy thunks
68
+ - Use `scope.use(value)(f)` to work with scoped values; returns `$[B]`
70
69
  - When the `scoped { ... }` block exits, finalizers run **LIFO** and errors are handled safely
70
+ - The `scoped` method requires `Unscoped[A]` evidence on the return type
71
71
 
72
72
  ---
73
73
 
74
74
  ## Core concepts
75
75
 
76
- ### 1) `Scope[ParentTag, Tag]`
76
+ ### 1) `Scope`
77
77
 
78
- A `Scope` manages finalizers and ties values to a *type-level identity* called a **Tag**.
78
+ `Scope` is a `sealed abstract class` with **no** type parameters. It manages finalizers and ties values to a *type-level identity* via abstract type members.
79
79
 
80
- - `Scope[ParentTag, Tag]` has **two** type parameters:
81
- - `ParentTag`: the parent scope's tag (capability boundary)
82
- - `Tag <: ParentTag`: this scope's unique identity (used to tag values)
80
+ - **`type $[+A]`** a path-dependent opaque type that tags values to this scope. Covariant in `A`. Equal to `A` at runtime (zero-cost).
81
+ - **`type Parent <: Scope`** — the parent scope's type.
82
+ - **`val parent: Parent`** reference to the parent scope.
83
83
 
84
- Every `Scope` also exposes a *path-dependent* member type:
84
+ Each scope instance exposes its own `$[+A]`, so a parent's `$[Database]` is a different type than a child's `$[Database]`, even though both equal `Database` at runtime.
85
85
 
86
86
  ```scala
87
- type Tag = Tag0
87
+ type $[+A] // = A at runtime (zero-cost)
88
88
  ```
89
89
 
90
90
  So in code you'll typically write:
91
91
 
92
92
  ```scala
93
93
  Scope.global.scoped { scope =>
94
- val x: Something @@ scope.Tag = ???
94
+ import scope._
95
+ val x: $[Something] = ??? // or scope.$[Something]
95
96
  }
96
97
  ```
97
98
 
99
+ Child scopes are represented by `Scope.Child[P <: Scope]`, a `final class` nested in the `Scope` companion object.
100
+
98
101
  #### Global scope
99
102
 
100
- `Scope.global` is the root of the tag hierarchy:
103
+ `Scope.global` is the root of the scope hierarchy:
101
104
 
102
105
  ```scala
103
106
  object Scope {
104
- type GlobalTag
105
- lazy val global: Scope[GlobalTag, GlobalTag]
107
+ object global extends Scope {
108
+ type $[+A] = A
109
+ type Parent = global.type
110
+ val parent: Parent = this
111
+ }
106
112
  }
107
113
  ```
108
114
 
109
115
  - The global scope is intended to live for the lifetime of the process.
110
116
  - Its finalizers run on JVM shutdown.
111
- - Values allocated in `Scope.global` can escape as raw values via `ScopeLift.globalScope` (see below).
112
117
 
113
118
  ---
114
119
 
115
- ### 2) Scoped values: `A @@ S`
120
+ ### 2) Scoped values: `$[+A]`
116
121
 
117
- `A @@ S` is a type alias for `Scoped[A, S]` a handle to a value of type `A` that is locked to scope tag `S`.
122
+ `$[+A]` (or `scope.$[A]` in type annotations) is a path-dependent opaque type representing a value of type `A` that is locked to a specific scope. It is covariant in `A`.
118
123
 
119
- - **Runtime representation:** a boxed thunk (lightweight wrapper)
120
- - **Key effect:** methods on `A` are hidden; you can't call `a.method` directly
121
- - **Acquisition timing:** `scope.allocate(resource)` acquires the resource **immediately** (eagerly) and returns a scoped handle for accessing the already-acquired value. The thunk defers *access*, not *acquisition*.
124
+ - **Runtime representation:** `$[A] = A` zero-cost opaque type, no boxing or wrapping
125
+ - **Key effect:** methods on `A` are hidden at the type level; you can't call `a.method` directly
126
+ - **All operations are eager:** `allocate(resource)` acquires the resource **immediately** and returns a scoped value
122
127
  - **Access paths:**
123
- - `(scope $ a)(f)` to execute and apply a function immediately
124
- - `a.map / a.flatMap` to build composite scoped computations
125
- - `scope.execute(scoped)` to run a composed computation
128
+ - `scope.use(a)(f)` to apply a function and get `$[B]`
129
+
130
+ #### `ScopedOps`: `map` and `flatMap` on `$[A]`
131
+
132
+ `Scope` provides an implicit class `ScopedOps[A]` that adds `map` and `flatMap` to `$[A]` values, enabling for-comprehension syntax:
133
+
134
+ ```scala
135
+ Scope.global.scoped { scope =>
136
+ import scope._
137
+
138
+ val x: $[Int] = $(42)
139
+ val y: $[String] = x.map(_.toString)
140
+ val z: $[String] = x.flatMap(v => $(s"value: $v"))
141
+ }
142
+ ```
143
+
144
+ - `sa.map(f: A => B): $[B]` — applies `f` to the unwrapped value, re-wraps the result
145
+ - `sa.flatMap(f: A => $[B]): $[B]` — applies `f` to the unwrapped value (where `f` returns a scoped value)
146
+ - All operations are **eager** (zero-cost)
126
147
 
127
148
  #### Scala 2 note
128
149
 
129
- In Scala 2, explicit type annotations are required when assigning scoped values to avoid existential type inference issues:
150
+ In Scala 2, the `scoped` method must be called with a lambda literal. Passing a variable or method reference is not supported due to macro limitations:
130
151
 
131
152
  ```scala
132
- // Scala 2 requires explicit type annotation
133
- val db: Database @@ scope.Tag = scope.allocate(Resource[Database])
153
+ // OK: lambda literal
154
+ Scope.global.scoped { scope => ... }
134
155
 
135
- // Scala 3 can infer the type
136
- val db = scope.allocate(Resource[Database])
156
+ // ❌ ERROR in Scala 2 (works in Scala 3):
157
+ val f: Scope.Child[_] => Any = scope => ...
158
+ Scope.global.scoped(f)
137
159
  ```
138
160
 
139
161
  ---
@@ -143,7 +165,7 @@ val db = scope.allocate(Resource[Database])
143
165
  `Resource[A]` describes how to **acquire** an `A` and how to **release** it when a scope closes. It is intentionally lazy: you *describe what to do*, and allocation happens only through:
144
166
 
145
167
  ```scala
146
- scope.allocate(resource)
168
+ allocate(resource)
147
169
  ```
148
170
 
149
171
  Common constructors:
@@ -177,48 +199,9 @@ Common constructors:
177
199
 
178
200
  ---
179
201
 
180
- ### 4) `A @@ S`: unified scoped type
181
-
182
- `A @@ S` (type alias for `Scoped[A, S]`) is the core type representing a deferred computation that produces `A` and requires scope tag `S` to execute.
183
-
184
- Execution happens via:
185
-
186
- ```scala
187
- scope.execute(scopedComputation)
188
- ```
189
-
190
- How to build them:
191
-
192
- - From `scope.allocate`:
193
- - `scope.allocate(resource)` returns `A @@ scope.Tag`
194
- - Using combinators:
195
- - `(a: A @@ S).map(f: A => B)` returns `B @@ S`
196
- - `(a: A @@ S).flatMap(f: A => B @@ T)` returns `B @@ (S & T)` (Scala 3) / `B @@ (S with T)` (Scala 2)
197
- - Use for-comprehensions to chain scoped computations
198
- - From ordinary values:
199
- - `Scoped(value)` lifts a value into an `A @@ Any` (which can be used anywhere due to contravariance)
200
-
201
- **Contravariance:** `A @@ S` is contravariant in `S`. This means `A @@ ParentTag` is a subtype of `A @@ ChildTag` when `ChildTag <: ParentTag`. Child scopes can execute parent-scoped computations automatically.
202
+ ### 4) `Unscoped`: marking pure data types
202
203
 
203
- **For-comprehension example:**
204
-
205
- ```scala
206
- Scope.global.scoped { scope =>
207
- val program: Result @@ scope.Tag = for {
208
- pool <- scope.allocate(Resource[Pool])
209
- conn <- scope.allocate(Resource(pool.lease()))
210
- data <- conn.map(_.query("SELECT *"))
211
- } yield process(data)
212
-
213
- scope.execute(program)
214
- }
215
- ```
216
-
217
- ---
218
-
219
- ### 5) `Unscoped`: marking pure data types
220
-
221
- The `Unscoped[A]` typeclass marks types as pure data that don't hold resources. This is used by `ScopeLift` to determine what can escape from child scopes.
204
+ The `Unscoped[A]` typeclass marks types as pure data that don't hold resources. The `scoped` method requires `Unscoped[A]` evidence on the return type to ensure only safe values can exit a scope.
222
205
 
223
206
  **Built-in Unscoped types:**
224
207
  - Primitives: `Int`, `Long`, `Boolean`, `Double`, etc.
@@ -227,71 +210,76 @@ The `Unscoped[A]` typeclass marks types as pure data that don't hold resources.
227
210
 
228
211
  **Custom Unscoped types:**
229
212
  ```scala
213
+ // Scala 3:
230
214
  case class Config(debug: Boolean)
231
215
  object Config {
232
216
  given Unscoped[Config] = Unscoped.derived
233
217
  }
218
+
219
+ // Scala 2:
220
+ case class Config(debug: Boolean)
221
+ object Config {
222
+ implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
223
+ }
234
224
  ```
235
225
 
236
- **Important:** `$` and `execute` always return `B @@ scope.Tag`. They never escape values directly. Escape only happens at the `.scoped` boundary via `ScopeLift`.
226
+ **Allowed return types from `scoped`:**
237
227
 
238
- ---
228
+ - **`Unscoped` types**: Pure data that can safely exit
229
+ - **`Nothing`**: For blocks that throw
239
230
 
240
- ### 6) `ScopeLift[A, S]`: what may return from child scopes
231
+ **Rejected return types (no Unscoped instance):**
241
232
 
242
- When you call `scope.scoped { child => ... }`, the return type `A` must have a `ScopeLift[A, S]` instance where `S` is the parent scope's tag. This typeclass both **gates** what can exit child scopes and **transforms** the return type.
233
+ - **Closures**: `() => A` could capture the child scope
234
+ - **Scoped values**: `$[A]` would be use-after-close
235
+ - **The scope itself**: Would allow operations after close
243
236
 
244
- **ScopeLift instances and their behavior:**
237
+ ```scala
238
+ Scope.global.scoped { parent =>
239
+ import parent._
245
240
 
246
- | Instance | Matches | Output Type |
247
- |----------|---------|-------------|
248
- | `globalScope[A]` | Any `A` when `S = GlobalTag` | `A` (unchanged) |
249
- | `nothing[S]` | `Nothing` | `Nothing` |
250
- | `unscoped[A, S]` | `A` with `Unscoped[A]` | `A` (raw value) |
251
- | `scoped[B, T, S]` | `B @@ T` when `S <:< T` | `B @@ T` (unchanged) |
241
+ // OK: String is Unscoped
242
+ val result: String = scoped { child =>
243
+ "hello"
244
+ }
252
245
 
253
- **Allowed return types:**
246
+ // COMPILE ERROR: $[Database] has no Unscoped instance
247
+ // val escaped = scoped { child =>
248
+ // import child._
249
+ // allocate(Resource(new Database)) // $[Database] can't escape
250
+ // }
251
+ }
252
+ ```
254
253
 
255
- - **`Unscoped` types**: Pure data lifts to raw `A`
256
- - **Parent-scoped values**: `B @@ T` where `S <:< T` lifts as-is
257
- - **`Nothing`**: For blocks that throw
258
- - **Anything from global scope**: Global scope never closes
254
+ ---
259
255
 
260
- **Rejected return types (no ScopeLift instance):**
256
+ ### 5) `lower`: accessing parent-scoped values
261
257
 
262
- - **Closures**: `() => A` could capture the child scope
263
- - **Child-scoped values**: `B @@ child.Tag` would be use-after-close
264
- - **The scope itself**: Would allow operations after close
258
+ When working in a child scope, you may need to access values allocated in a parent scope. Use `lower(parentValue)` to "lower" a parent-scoped value into the child scope:
265
259
 
266
260
  ```scala
267
261
  Scope.global.scoped { parent =>
268
- // ✅ OK: String is Unscoped - lifts to raw String
269
- val result: String = parent.scoped { child =>
270
- "hello" // Return raw Unscoped value
271
- }
262
+ import parent._
272
263
 
273
- // ✅ OK: parent-tagged value outlives child - lifts as-is
274
- val parentDb: Database @@ parent.Tag = parent.allocate(Resource[Database])
275
- val same: Database @@ parent.Tag = parent.scoped { _ =>
276
- parentDb
277
- }
264
+ val parentDb: $[Database] = allocate(Resource(new Database))
278
265
 
279
- // COMPILE ERROR: closure has no ScopeLift instance
280
- // val leak = parent.scoped { child =>
281
- // val db = child.allocate(Resource[Database])
282
- // () => println("captured!") // Function types rejected
283
- // }
266
+ scoped { child =>
267
+ import child._
284
268
 
285
- // COMPILE ERROR: child-scoped value has no ScopeLift instance
286
- // val escaped = parent.scoped { child =>
287
- // child.allocate(Resource[Database]) // Database @@ child.Tag can't escape
288
- // }
269
+ // Use lower() to access parent-scoped value in child scope
270
+ val db: $[Database] = lower(parentDb)
271
+ child.use(db)(_.query("SELECT 1"))
272
+
273
+ "done"
274
+ }
289
275
  }
290
276
  ```
291
277
 
278
+ The `lower` operation is necessary because each scope has its own `$[A]` opaque type. A parent's `$[A]` is a different type than a child's `$[A]`, even though both equal `A` at runtime.
279
+
292
280
  ---
293
281
 
294
- ### 7) `Wire[-In, +Out]`: dependency recipes
282
+ ### 6) `Wire[-In, +Out]`: dependency recipes
295
283
 
296
284
  `Wire` is a recipe for constructing services. It describes **how** to build a service given its dependencies, but does not resolve those dependencies itself.
297
285
 
@@ -351,22 +339,24 @@ Child scopes are created with:
351
339
 
352
340
  ```scala
353
341
  Scope.global.scoped { scope =>
354
- scope.scoped { child =>
342
+ import scope._
343
+ scoped { child =>
344
+ import child._
355
345
  // allocate in child
356
346
  }
357
347
  }
358
348
  ```
359
349
 
360
- The child scope has an existential tag (fresh per invocation). You can allocate in the child, but you can't return those values to the parent in a usable form because the parent cannot name (or satisfy) the child tag.
350
+ The child scope has a fresh, unnameable `$[A]` type (created per invocation). You can allocate in the child, but you can't return those values to the parent in a usable form because the parent cannot name (or satisfy) the child's `$[A]` type.
361
351
 
362
352
  Compile-time safety is verified in tests, e.g.:
363
353
  `ScopeCompileTimeSafetyScala3Spec`.
364
354
 
365
- ### B) Contravariance prevents child-to-parent widening
355
+ ### B) Opaque types prevent escape
366
356
 
367
- `A @@ S` is contravariant in `S`. This means `Db @@ child.Tag` is **not** a subtype of `Db @@ parent.Tag` the subtyping goes the *other* direction. A child scope can use parent-tagged values (because `child.Tag <: parent.Tag` makes `A @@ parent.Tag <: A @@ child.Tag`), but you cannot widen a child-tagged value to a parent tag.
357
+ Each scope defines its own `$[A]` opaque type. Even though `$[A] = A` at runtime, the compiler treats each scope's `$[A]` as distinct. A child's `$[Database]` is a different type than the parent's `$[Database]`.
368
358
 
369
- Additionally, the thunk-based representation hides `A`'s methods — you can't call `db.query(...)` directly on a `Database @@ scope.Tag`. The only sanctioned access routes are `scope.$` and `scope.execute`, which require a scope with a compatible tag.
359
+ Additionally, the opaque type hides `A`'s methods at the type level — you can't call `db.query(...)` directly on a `$[Database]`. Access routes are `scope.use(value)(f)` and the `ScopedOps` methods (`map`, `flatMap`) for for-comprehensions.
370
360
 
371
361
  ---
372
362
 
@@ -383,13 +373,13 @@ final class FileHandle(path: String) extends AutoCloseable {
383
373
  }
384
374
 
385
375
  Scope.global.scoped { scope =>
386
- val h = scope.allocate(Resource(new FileHandle("data.txt")))
376
+ import scope._
387
377
 
388
- // $ executes immediately - work with the value inside the function
389
- (scope $ h) { handle =>
390
- val contents = handle.readAll()
391
- println(contents)
392
- }
378
+ val h: $[FileHandle] = allocate(Resource(new FileHandle("data.txt")))
379
+
380
+ // scope.use applies function to scoped value, returns $[String]
381
+ val contents: $[String] = scope.use(h)(_.readAll())
382
+ println(contents) // $[String] = String at runtime
393
383
  }
394
384
  ```
395
385
 
@@ -401,66 +391,45 @@ Scope.global.scoped { scope =>
401
391
  import zio.blocks.scope._
402
392
 
403
393
  Scope.global.scoped { parent =>
404
- val parentDb = parent.allocate(Resource(new Database))
394
+ import parent._
405
395
 
406
- parent.scoped { child =>
407
- // child can use parent-scoped values:
408
- child.$(parentDb) { db =>
409
- println(db.query("SELECT 1"))
410
- }
396
+ val parentDb: $[Database] = allocate(Resource(new Database))
411
397
 
412
- val childDb = child.allocate(Resource(new Database))
398
+ scoped { child =>
399
+ import child._
400
+
401
+ // Use lower() to access parent-scoped values in child scope:
402
+ val db: $[Database] = lower(parentDb)
403
+ println(child.use(db)(_.query("SELECT 1")))
404
+
405
+ val childDb: $[Database] = allocate(Resource(new Database))
413
406
 
414
407
  // You can use childDb *inside* the child:
415
- child.$(childDb) { db =>
416
- println(db.query("SELECT 2"))
417
- }
408
+ println(child.use(childDb)(_.query("SELECT 2")))
418
409
 
419
- // Return an Unscoped value - ScopeLift extracts it
420
- "done"
421
-
422
410
  // But you cannot return childDb to the parent:
423
- // childDb : Database @@ child.Tag has no ScopeLift instance
411
+ // $[Database] has no Unscoped instance — compile error
412
+
413
+ // Return an Unscoped value
414
+ "done"
424
415
  }
425
416
 
426
417
  // parentDb is still usable here:
427
- parent.$(parentDb) { db =>
428
- println(db.query("SELECT 3"))
429
- }
418
+ println(parent.use(parentDb)(_.query("SELECT 3")))
430
419
  }
431
420
  ```
432
421
 
433
422
  ---
434
423
 
435
- ### Building a `Scoped` program (map/flatMap)
424
+ ### Chaining resource acquisition
436
425
 
437
- You can use for-comprehensions to compose scoped computations. Each `<-` uses `flatMap`, accumulating requirements:
426
+ Since `$[A]` supports `map` and `flatMap` via `ScopedOps`, you can chain resource acquisitions in for-comprehensions:
438
427
 
439
428
  ```scala
440
429
  import zio.blocks.scope._
441
430
 
442
- Scope.global.scoped { scope =>
443
- val db: Database @@ scope.Tag = scope.allocate(Resource(new Database))
444
-
445
- // Build a scoped computation with map
446
- val program: String @@ scope.Tag = db.map { d =>
447
- d.query("SELECT 1")
448
- }
449
-
450
- // execute runs the computation immediately, returns String @@ scope.Tag
451
- // Use side effects inside the computation to observe results
452
- scope.execute(program)
453
- }
454
- ```
455
-
456
- This pattern shines when chaining resource acquisition:
457
-
458
- ```scala
459
- import zio.blocks.scope._
460
-
461
- // A pool that leases connections
462
- class Pool(implicit finalizer: Finalizer) extends AutoCloseable {
463
- def lease: Resource[Connection] = Resource(new Connection)
431
+ class Pool extends AutoCloseable {
432
+ def lease(): Connection = new Connection
464
433
  def close(): Unit = println("pool closed")
465
434
  }
466
435
 
@@ -470,33 +439,36 @@ class Connection extends AutoCloseable {
470
439
  }
471
440
 
472
441
  Scope.global.scoped { scope =>
473
- val pool: Pool @@ scope.Tag = scope.allocate(Resource.from[Pool])
442
+ import scope._
474
443
 
475
- val program: String @@ scope.Tag =
476
- for {
477
- p <- pool // extract Pool from scoped
478
- connection <- scope.allocate(p.lease) // allocate returns Connection @@ Tag
479
- } yield connection.query("SELECT 1")
444
+ // Chain allocations in a for-comprehension:
445
+ // flatMap unwraps $[Pool] to Pool, so pool.lease() returns a raw Connection
446
+ val result: $[String] = for {
447
+ pool <- allocate(Resource.from[Pool])
448
+ conn <- allocate(Resource(pool.lease()))
449
+ } yield conn.query("SELECT 1")
480
450
 
481
- // execute runs the computation - connection is used inside the yield
482
- scope.execute(program)
451
+ println(result)
483
452
  }
484
- // Output: connection closed, then pool closed (LIFO)
453
+ // Output: result: SELECT 1
454
+ // Then: connection closed, pool closed (LIFO)
485
455
  ```
486
456
 
487
457
  ---
488
458
 
489
459
  ### Registering cleanup manually with `defer`
490
460
 
491
- Use `scope.defer` when you already have a value and just need to register cleanup.
461
+ Use `defer` when you already have a value and just need to register cleanup.
492
462
 
493
463
  ```scala
494
464
  import zio.blocks.scope._
495
465
 
496
466
  Scope.global.scoped { scope =>
467
+ import scope._
468
+
497
469
  val handle = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
498
470
 
499
- scope.defer { handle.close() }
471
+ defer { handle.close() }
500
472
 
501
473
  val firstByte = handle.read()
502
474
  println(firstByte)
@@ -509,6 +481,7 @@ There is also a package-level helper `defer` that only requires a `Finalizer`:
509
481
  import zio.blocks.scope._
510
482
 
511
483
  Scope.global.scoped { scope =>
484
+ import scope._
512
485
  given Finalizer = scope
513
486
 
514
487
  defer { println("cleanup") }
@@ -526,7 +499,7 @@ import zio.blocks.scope._
526
499
 
527
500
  class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
528
501
  private val pool = createPool(config)
529
- defer { pool.shutdown() }
502
+ finalizer.defer { pool.shutdown() } // or: defer { ... } with import zio.blocks.scope._
530
503
 
531
504
  def getConnection(): Connection = pool.acquire()
532
505
  }
@@ -535,14 +508,15 @@ class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
535
508
  val resource = Resource.from[ConnectionPool](Wire(Config("jdbc://localhost")))
536
509
 
537
510
  Scope.global.scoped { scope =>
538
- val pool = scope.allocate(resource)
511
+ import scope._
512
+ val pool = allocate(resource)
539
513
  // pool.shutdown() will be called when scope closes
540
514
  }
541
515
  ```
542
516
 
543
517
  Why `Finalizer` instead of `Scope`?
544
518
  - `Finalizer` is the minimal interface—it only has `defer`
545
- - Classes that need cleanup should not have access to `allocate` or `$`
519
+ - Classes that need cleanup should not have access to `allocate` or `use`
546
520
  - The macros pass a `Finalizer` at runtime, so declaring `Scope` would be misleading
547
521
 
548
522
  ---
@@ -561,13 +535,13 @@ val w: Wire.Shared[Boolean, Config] = Wire.shared[Config]
561
535
  val deps: Context[Boolean] = Context[Boolean](true)
562
536
 
563
537
  Scope.global.scoped { scope =>
564
- val cfg: Config @@ scope.Tag =
565
- scope.allocate(w.toResource(deps))
538
+ import scope._
539
+
540
+ val cfg: $[Config] = allocate(w.toResource(deps))
566
541
 
567
- val debug: Boolean =
568
- (scope $ cfg)(_.debug) // Boolean typically escapes
542
+ val debug: $[Boolean] = scope.use(cfg)(_.debug)
569
543
 
570
- println(debug)
544
+ println(debug) // $[Boolean] = Boolean at runtime
571
545
  }
572
546
  ```
573
547
 
@@ -612,8 +586,9 @@ val serviceResource: Resource[Service] =
612
586
  )
613
587
 
614
588
  Scope.global.scoped { scope =>
615
- val svc = scope.allocate(serviceResource)
616
- (scope $ svc)(_.run())
589
+ import scope._
590
+ val svc: $[Service] = allocate(serviceResource)
591
+ scope.use(svc)(_.run())
617
592
  }
618
593
  // Output: running with [jdbc:postgresql://localhost/db] SELECT 1
619
594
  // Then: service closed, database closed (LIFO order)
@@ -655,8 +630,9 @@ val appResource: Resource[App] =
655
630
  )
656
631
 
657
632
  Scope.global.scoped { scope =>
658
- val app = scope.allocate(appResource)
659
- (scope $ app)(_.run())
633
+ import scope._
634
+ val app: $[App] = allocate(appResource)
635
+ scope.use(app)(_.run())
660
636
  }
661
637
  ```
662
638
 
@@ -680,13 +656,14 @@ val appResource = Resource.from[App](
680
656
 
681
657
  ### Interop escape hatch: `leak`
682
658
 
683
- Sometimes you must hand a raw value to code that cannot work with `@@` types.
659
+ Sometimes you must hand a raw value to code that cannot work with `$[A]` types.
684
660
 
685
661
  ```scala
686
662
  import zio.blocks.scope._
687
663
 
688
664
  Scope.global.scoped { scope =>
689
- val db = scope.allocate(Resource(new Database))
665
+ import scope._
666
+ val db: $[Database] = allocate(Resource(new Database))
690
667
 
691
668
  val raw: Database = leak(db) // emits a compiler warning
692
669
  // thirdParty(raw)
@@ -723,8 +700,8 @@ The scope macros produce beautiful, actionable compile-time error messages with
723
700
  This type (primitive, collection, or function) cannot be auto-created.
724
701
 
725
702
  Required by:
726
- └── Config
727
- └── App
703
+ ├── Config
704
+ └── App
728
705
 
729
706
  Fix: Provide Wire(value) with the desired value:
730
707
 
@@ -746,7 +723,7 @@ The scope macros produce beautiful, actionable compile-time error messages with
746
723
  This type is abstract (trait or abstract class).
747
724
 
748
725
  Required by:
749
- └── App
726
+ └── App
750
727
 
751
728
  Fix: Provide a wire for a concrete implementation:
752
729
 
@@ -852,7 +829,7 @@ When using `leak(value)` to escape the scoped type system:
852
829
 
853
830
  Hint:
854
831
  If you know this data type is not resourceful, then add an Unscoped
855
- instance for it so ScopeLift can lift it automatically.
832
+ instance for it so you do not need to leak it.
856
833
 
857
834
  ────────────────────────────────────────────────────────────────────────────
858
835
  ```
@@ -866,22 +843,42 @@ When using `leak(value)` to escape the scoped type system:
866
843
  Core methods (Scala 3 `using` vs Scala 2 `implicit` differs, but the shapes are the same):
867
844
 
868
845
  ```scala
869
- final class Scope[ParentTag, Tag0 <: ParentTag] {
870
- type Tag = Tag0
846
+ sealed abstract class Scope extends Finalizer {
847
+ type $[+A] // = A at runtime (zero-cost)
848
+ type Parent <: Scope
849
+ val parent: Parent
871
850
 
872
- def allocate[A](resource: Resource[A]): A @@ Tag
851
+ def allocate[A](resource: Resource[A]): $[A]
852
+ def allocate[A <: AutoCloseable](value: => A): $[A]
873
853
  def defer(f: => Unit): Unit
874
854
 
875
- // Apply function to scoped value - executes immediately, always returns scoped
876
- def $[A, B](scoped: A @@ this.Tag)(f: A => B): B @@ Tag
855
+ // Apply function to scoped value, returns scoped result
856
+ def use[A, B](scoped: $[A])(f: A => B): $[B]
877
857
 
878
- // Execute scoped computation - runs immediately, always returns scoped
879
- def execute[A](scoped: A @@ this.Tag): A @@ Tag
858
+ // Construct a scoped value from a raw value
859
+ def $[A](a: A): $[A]
860
+
861
+ // Lower parent-scoped value into this scope
862
+ def lower[A](value: parent.$[A]): $[A]
863
+
864
+ // Escape hatch: unwrap scoped value (emits compiler warning)
865
+ // Scala 3:
866
+ inline def leak[A](inline sa: $[A]): A // macro — emits warning
867
+ // Scala 2:
868
+ def leak[A](sa: $[A]): A // macro — emits warning
869
+
870
+ // Creates a child scope - requires Unscoped evidence on return type
871
+ // Scala 3:
872
+ def scoped[A](f: (child: Scope.Child[self.type]) => child.$[A])(using Unscoped[A]): A
873
+ // Scala 2 (macro rewrites the types; declared signature is untyped):
874
+ def scoped(f: Scope.Child[self.type] => Any): Any // macro
875
+
876
+ implicit class ScopedOps[A](sa: $[A]) {
877
+ def map[B](f: A => B): $[B]
878
+ def flatMap[B](f: A => $[B]): $[B]
879
+ }
880
880
 
881
- // Creates a child scope - ScopeLift controls what may exit
882
- def scoped[A](f: Scope[this.Tag, ? <: this.Tag] => A)(
883
- using lift: ScopeLift[A, this.Tag]
884
- ): lift.Out
881
+ implicit def wrapUnscoped[A: Unscoped](a: A): $[A]
885
882
  }
886
883
  ```
887
884
 
@@ -895,8 +892,9 @@ object Resource {
895
892
  def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
896
893
  def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
897
894
 
898
- // Macro - DI entry point (can also be called with no args for zero-dep classes):
899
- def from[T](wires: Wire[?, ?]*): Resource[T]
895
+ // Macro - DI entry point:
896
+ def from[T]: Resource[T] // zero-dep classes
897
+ def from[T](wires: Wire[?, ?]*): Resource[T] // with dependency wires
900
898
 
901
899
  // Internal (used by generated code):
902
900
  def shared[A](f: Finalizer => A): Resource[A]
@@ -916,14 +914,14 @@ sealed trait Wire[-In, +Out] {
916
914
 
917
915
  object Wire {
918
916
  // Macro entry points:
919
- def shared[T]: Wire[?, T] // derive from T's constructor
920
- def unique[T]: Wire[?, T] // derive from T's constructor
917
+ def shared[T]: Wire.Shared[?, T] // derive from T's constructor
918
+ def unique[T]: Wire.Unique[?, T] // derive from T's constructor
921
919
 
922
920
  // Wrap pre-existing value (auto-finalizes if AutoCloseable):
923
921
  def apply[T](value: T): Wire.Shared[Any, T]
924
922
 
925
- final class Shared[-In, +Out] extends Wire[In, Out]
926
- final class Unique[-In, +Out] extends Wire[In, Out]
923
+ final case class Shared[-In, +Out] extends Wire[In, Out]
924
+ final case class Unique[-In, +Out] extends Wire[In, Out]
927
925
  }
928
926
  ```
929
927
 
@@ -931,13 +929,13 @@ object Wire {
931
929
 
932
930
  ## Mental model recap
933
931
 
934
- - Use `Scope.global.scoped { scope => ... }` to create a safe region.
935
- - For simple resources: `scope.allocate(Resource(value))` or `scope.allocate(Resource.acquireRelease(...)(...))`
936
- - For dependency injection: `scope.allocate(Resource.from[App](Wire(config), ...))` — auto-wires concrete classes, you provide leaves and overrides.
937
- - Use scoped values via `(scope $ value) { v => ... }`the function executes immediately.
938
- - `$` and `execute` always return scoped values (`B @@ scope.Tag`), never raw values.
939
- - Escape only happens at `.scoped` boundaries via `ScopeLift`.
940
- - Nest with `scope.scoped { child => ... }` to create a tighter lifetime boundary.
932
+ - Use `Scope.global.scoped { scope => import scope._; ... }` to create a safe region.
933
+ - For simple resources: `allocate(Resource(value))` or `allocate(Resource.acquireRelease(...)(...))`
934
+ - For dependency injection: `allocate(Resource.from[App](Wire(config), ...))` — auto-wires concrete classes, you provide leaves and overrides.
935
+ - Use `scope.use(value)(f)` to work with scoped valuesall operations are eager.
936
+ - `$[A] = A` at runtime zero-cost opaque type.
937
+ - The `scoped` method requires `Unscoped[A]` evidence on the return type.
938
+ - Use `lower(parentValue)` to access parent-scoped values in child scopes.
941
939
  - Return `Unscoped` types from child scopes to extract raw values.
942
940
  - If it doesn't typecheck, it would have been unsafe at runtime.
943
941