@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/index.md +49 -29
- package/package.json +1 -1
- package/path-interpolator.md +24 -23
- package/reference/codec.md +384 -0
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +392 -0
- package/reference/formats.md +68 -12
- package/reference/json-schema.md +13 -10
- package/reference/lazy.md +361 -0
- package/reference/modifier.md +340 -0
- package/reference/syntax.md +11 -11
- package/reference/type-class-derivation.md +1959 -0
- package/scope.md +230 -232
- package/sidebars.js +6 -1
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
|
-
- **
|
|
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
|
|
20
|
-
- [2) Scoped values:
|
|
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) `
|
|
23
|
-
- [5) `
|
|
24
|
-
- [6) `
|
|
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
|
-
- [
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
- `
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
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
|
|
76
|
+
### 1) `Scope`
|
|
77
77
|
|
|
78
|
-
|
|
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
|
-
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
103
|
+
`Scope.global` is the root of the scope hierarchy:
|
|
101
104
|
|
|
102
105
|
```scala
|
|
103
106
|
object Scope {
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
120
|
+
### 2) Scoped values: `$[+A]`
|
|
116
121
|
|
|
117
|
-
`
|
|
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:**
|
|
120
|
-
- **Key effect:** methods on `A` are hidden; you can't call `a.method` directly
|
|
121
|
-
- **
|
|
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
|
-
- `(
|
|
124
|
-
|
|
125
|
-
|
|
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,
|
|
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
|
-
//
|
|
133
|
-
|
|
153
|
+
// ✅ OK: lambda literal
|
|
154
|
+
Scope.global.scoped { scope => ... }
|
|
134
155
|
|
|
135
|
-
// Scala
|
|
136
|
-
val
|
|
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
|
-
|
|
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) `
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
231
|
+
**Rejected return types (no Unscoped instance):**
|
|
241
232
|
|
|
242
|
-
|
|
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
|
-
|
|
237
|
+
```scala
|
|
238
|
+
Scope.global.scoped { parent =>
|
|
239
|
+
import parent._
|
|
245
240
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
256
|
+
### 5) `lower`: accessing parent-scoped values
|
|
261
257
|
|
|
262
|
-
|
|
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
|
-
|
|
269
|
-
val result: String = parent.scoped { child =>
|
|
270
|
-
"hello" // Return raw Unscoped value
|
|
271
|
-
}
|
|
262
|
+
import parent._
|
|
272
263
|
|
|
273
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
// val db = child.allocate(Resource[Database])
|
|
282
|
-
// () => println("captured!") // Function types rejected
|
|
283
|
-
// }
|
|
266
|
+
scoped { child =>
|
|
267
|
+
import child._
|
|
284
268
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
###
|
|
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.
|
|
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
|
|
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)
|
|
355
|
+
### B) Opaque types prevent escape
|
|
366
356
|
|
|
367
|
-
|
|
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
|
|
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
|
-
|
|
376
|
+
import scope._
|
|
387
377
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
394
|
+
import parent._
|
|
405
395
|
|
|
406
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
###
|
|
424
|
+
### Chaining resource acquisition
|
|
436
425
|
|
|
437
|
-
|
|
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
|
-
|
|
443
|
-
|
|
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
|
-
|
|
442
|
+
import scope._
|
|
474
443
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
482
|
-
scope.execute(program)
|
|
451
|
+
println(result)
|
|
483
452
|
}
|
|
484
|
-
// Output:
|
|
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 `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
565
|
-
|
|
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
|
-
|
|
616
|
-
|
|
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
|
-
|
|
659
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
870
|
-
type
|
|
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
|
|
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
|
|
876
|
-
def
|
|
855
|
+
// Apply function to scoped value, returns scoped result
|
|
856
|
+
def use[A, B](scoped: $[A])(f: A => B): $[B]
|
|
877
857
|
|
|
878
|
-
//
|
|
879
|
-
def
|
|
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
|
-
|
|
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
|
|
899
|
-
def from[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: `
|
|
936
|
-
- For dependency injection: `
|
|
937
|
-
- Use
|
|
938
|
-
-
|
|
939
|
-
-
|
|
940
|
-
-
|
|
932
|
+
- Use `Scope.global.scoped { scope => import scope._; ... }` to create a safe region.
|
|
933
|
+
- For simple resources: `allocate(Resource(value))` or `allocate(Resource.acquireRelease(...)(...))`
|
|
934
|
+
- For dependency injection: `allocate(Resource.from[App](Wire(config), ...))` — auto-wires concrete classes, you provide leaves and overrides.
|
|
935
|
+
- Use `scope.use(value)(f)` to work with scoped values — all operations are eager.
|
|
936
|
+
- `$[A] = A` at runtime — zero-cost opaque type.
|
|
937
|
+
- The `scoped` method requires `Unscoped[A]` evidence on the return type.
|
|
938
|
+
- Use `lower(parentValue)` to access parent-scoped values in child scopes.
|
|
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
|
|