@zio.dev/zio-blocks 0.0.21 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/guides/query-dsl-extending.md +758 -0
- package/guides/query-dsl-fluent-builder.md +1287 -0
- package/guides/query-dsl-reified-optics.md +494 -0
- package/guides/query-dsl-sql.md +680 -0
- package/index.md +57 -34
- package/package.json +1 -1
- package/path-interpolator.md +24 -23
- package/reference/codec.md +386 -0
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +396 -0
- package/reference/formats.md +68 -12
- package/reference/json-schema.md +14 -11
- package/reference/json.md +2 -2
- package/reference/lazy.md +361 -0
- package/reference/media-type.md +460 -0
- package/reference/modifier.md +340 -0
- package/reference/optics.md +4 -0
- package/reference/schema-expr.md +669 -0
- package/reference/schema.md +1 -0
- package/reference/syntax.md +11 -11
- package/reference/type-class-derivation.md +1960 -0
- package/scope.md +754 -502
- package/sidebars.js +18 -1
- package/undocumented-report.md +331 -0
package/scope.md
CHANGED
|
@@ -1,396 +1,466 @@
|
|
|
1
|
-
# ZIO Blocks — Scope (
|
|
1
|
+
# ZIO Blocks — Scope (`zio.blocks.scope`)
|
|
2
2
|
|
|
3
|
-
`zio.blocks.scope`
|
|
3
|
+
`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.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
At runtime the model stays simple:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Allocate eagerly** (no lazy thunks)
|
|
8
|
+
- **Register finalizers**
|
|
9
|
+
- **Run finalizers deterministically** when a scope closes (**LIFO** order)
|
|
10
|
+
- Collect finalizer failures into a `Finalization` (and throw/suppress appropriately)
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- **Unified scoped type** (`A @@ S` is a type alias for `Scoped[A, S]`)
|
|
11
|
-
- **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
|
|
12
|
+
## Why Scope?
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Most resource bugs in Scala are "escape" bugs:
|
|
15
|
+
|
|
16
|
+
- storing a connection/stream in a field and using it after it was closed
|
|
17
|
+
- capturing a resource in a closure that outlives a scope
|
|
18
|
+
- passing a resource to code that might retain it
|
|
19
|
+
- mixing values from different lifetimes ("which scope owns this?")
|
|
20
|
+
|
|
21
|
+
Scope addresses these with a *tight* design:
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- [7) `Wire[-In, +Out]`: dependency recipes](#7-wire-in-out-dependency-recipes)
|
|
26
|
-
- [Safety model (why leaking is prevented)](#safety-model-why-leaking-is-prevented)
|
|
27
|
-
- [Usage examples](#usage-examples)
|
|
28
|
-
- [Allocating and using a resource](#allocating-and-using-a-resource)
|
|
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)
|
|
31
|
-
- [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
|
|
32
|
-
- [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
|
|
33
|
-
- [Dependency injection with `Resource.from[T](wires*)`](#dependency-injection-with-resourcefromtwires)
|
|
34
|
-
- [Injecting traits via subtype wires](#injecting-traits-via-subtype-wires)
|
|
35
|
-
- [Interop escape hatch: `leak`](#interop-escape-hatch-leak)
|
|
36
|
-
- [Common compile errors](#common-compile-errors)
|
|
37
|
-
- [API reference (selected)](#api-reference-selected)
|
|
23
|
+
| Feature | `zio.blocks.scope` |
|
|
24
|
+
|---|---|
|
|
25
|
+
| Compile-time leak prevention | ✓ (`scope.$[A]` + `$` macro + `Unscoped` boundary) |
|
|
26
|
+
| Runtime overhead | ~0 (scoped values erase to `A`) |
|
|
27
|
+
| Allocation model | Eager (allocation happens at `allocate`) |
|
|
28
|
+
| Finalization | Deterministic, LIFO, errors collected |
|
|
29
|
+
| Structured lifetime | Parent/child scopes, `lower` for explicit lifetime widening |
|
|
30
|
+
| Escape hatch | `leak` (warns) |
|
|
31
|
+
|
|
32
|
+
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**.
|
|
38
33
|
|
|
39
34
|
---
|
|
40
35
|
|
|
41
|
-
## Quick start
|
|
36
|
+
## Quick start (Scala 3)
|
|
42
37
|
|
|
43
38
|
```scala
|
|
44
|
-
import zio.blocks.scope
|
|
39
|
+
import zio.blocks.scope.*
|
|
45
40
|
|
|
46
|
-
final class Database extends AutoCloseable
|
|
41
|
+
final class Database extends AutoCloseable:
|
|
47
42
|
def query(sql: String): String = s"result: $sql"
|
|
48
43
|
def close(): Unit = println("db closed")
|
|
49
|
-
}
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
val
|
|
53
|
-
|
|
45
|
+
@main def quickStart(): Unit =
|
|
46
|
+
val out: String =
|
|
47
|
+
Scope.global.scoped { scope =>
|
|
48
|
+
import scope.*
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
(scope $ db) { database =>
|
|
58
|
-
val result = database.query("SELECT 1")
|
|
59
|
-
println(result)
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
```
|
|
50
|
+
val db: $[Database] =
|
|
51
|
+
Resource.fromAutoCloseable(new Database).allocate
|
|
63
52
|
|
|
64
|
-
|
|
53
|
+
// Safe access: the lambda parameter can only be used as a receiver
|
|
54
|
+
$(db)(_.query("SELECT 1"))
|
|
55
|
+
}
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
70
|
-
- When the `scoped { ... }` block exits, finalizers run **LIFO** and errors are handled safely
|
|
57
|
+
println(out)
|
|
58
|
+
```
|
|
71
59
|
|
|
72
|
-
|
|
60
|
+
Key points:
|
|
73
61
|
|
|
74
|
-
|
|
62
|
+
- `allocate(...)` returns a **scoped value**: `scope.$[Database]` (or `$[Database]` after `import scope.*`).
|
|
63
|
+
- You **cannot** call `db.query(...)` directly on `$[Database]`.
|
|
64
|
+
- You use the `$` access operator: `$(db)(...)` (or `(scope $ db)(...)` without the import).
|
|
65
|
+
- The `scoped` block returns a plain `String` because `String: Unscoped`.
|
|
66
|
+
- Finalizers run when the block exits, in **LIFO** order.
|
|
75
67
|
|
|
76
|
-
|
|
68
|
+
---
|
|
77
69
|
|
|
78
|
-
|
|
70
|
+
## Core mental model
|
|
79
71
|
|
|
80
|
-
|
|
81
|
-
- `ParentTag`: the parent scope's tag (capability boundary)
|
|
82
|
-
- `Tag <: ParentTag`: this scope's unique identity (used to tag values)
|
|
72
|
+
### 1) `Scope`: finalizers + type identity
|
|
83
73
|
|
|
84
|
-
|
|
74
|
+
`Scope` is a finalizer registry plus a unique type identity:
|
|
85
75
|
|
|
86
|
-
|
|
87
|
-
type
|
|
88
|
-
```
|
|
76
|
+
- `type $[+A]` — a scope-tagged, path-dependent type (erases to `A` at runtime)
|
|
77
|
+
- `type Parent <: Scope` / `val parent: Parent` — the scope hierarchy
|
|
89
78
|
|
|
90
|
-
|
|
79
|
+
Every scope instance defines a **different** `$` type, so values from different scopes don't accidentally mix.
|
|
91
80
|
|
|
92
81
|
```scala
|
|
93
82
|
Scope.global.scoped { scope =>
|
|
94
|
-
|
|
83
|
+
import scope.*
|
|
84
|
+
val x: $[Int] = 1 // ok (in global, $[A] = A)
|
|
95
85
|
}
|
|
96
86
|
```
|
|
97
87
|
|
|
98
88
|
#### Global scope
|
|
99
89
|
|
|
100
|
-
`Scope.global` is the root
|
|
90
|
+
`Scope.global` is the root:
|
|
91
|
+
|
|
92
|
+
- In the global scope: `type $[+A] = A` (identity)
|
|
93
|
+
- On the JVM: global finalizers run on shutdown via a shutdown hook
|
|
94
|
+
- On Scala.js: there is no shutdown hook, so global finalizers are **not** run automatically
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
### 2) Scoped values: `scope.$[A]` / `$[A]`
|
|
99
|
+
|
|
100
|
+
A value of type `scope.$[A]` means:
|
|
101
|
+
|
|
102
|
+
> "This is an `A`, but it is only valid while `scope` is alive."
|
|
103
|
+
|
|
104
|
+
Properties:
|
|
105
|
+
|
|
106
|
+
- **Zero-cost**: `$[A]` is just `A` at runtime (casts/identity)
|
|
107
|
+
- **Incompatible across scopes**: `outer.$[A]` is not `inner.$[A]`
|
|
108
|
+
- **Methods are hidden** at the type level; you must use `$` to access
|
|
109
|
+
|
|
110
|
+
#### Access operator: `(scope $ value)(f)`
|
|
111
|
+
|
|
112
|
+
The intended way to use a scoped value is:
|
|
101
113
|
|
|
102
114
|
```scala
|
|
103
|
-
|
|
104
|
-
type GlobalTag
|
|
105
|
-
lazy val global: Scope[GlobalTag, GlobalTag]
|
|
106
|
-
}
|
|
115
|
+
(scope $ scopedValue)(a => a.method(...))
|
|
107
116
|
```
|
|
108
117
|
|
|
109
|
-
|
|
110
|
-
- Its finalizers run on JVM shutdown.
|
|
111
|
-
- Values allocated in `Scope.global` can escape as raw values via `ScopeLift.globalScope` (see below).
|
|
118
|
+
This is enforced by a macro that checks the lambda uses its parameter only in **receiver position**.
|
|
112
119
|
|
|
113
|
-
|
|
120
|
+
Allowed:
|
|
121
|
+
|
|
122
|
+
```scala
|
|
123
|
+
(scope $ db)(_.query("SELECT 1"))
|
|
124
|
+
(scope $ db)(d => d.query("a") + d.query("b"))
|
|
125
|
+
(scope $ db)(_.query("x").toUpperCase)
|
|
126
|
+
(scope $ db)(_.field) // field access is allowed
|
|
127
|
+
```
|
|
114
128
|
|
|
115
|
-
|
|
129
|
+
Rejected at compile time:
|
|
116
130
|
|
|
117
|
-
|
|
131
|
+
```scala
|
|
132
|
+
(scope $ db)(d => store(d)) // parameter used as an argument
|
|
133
|
+
(scope $ db)(d => () => d.query("x")) // captured in a nested lambda
|
|
134
|
+
(scope $ db)(d => d) // returning the parameter
|
|
135
|
+
(scope $ db)(d => { val x = d; 1 }) // binding/storing the parameter itself
|
|
136
|
+
```
|
|
118
137
|
|
|
119
|
-
-
|
|
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*.
|
|
122
|
-
- **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
|
|
138
|
+
##### "Auto-unwrap" rule (`Unscoped`)
|
|
126
139
|
|
|
127
|
-
|
|
140
|
+
`$` *auto-unwraps* when the result type is known to be safe data:
|
|
128
141
|
|
|
129
|
-
|
|
142
|
+
- if `B: Unscoped` → `(scope $ sa)(f)` returns **`B`**
|
|
143
|
+
- otherwise → it returns **`scope.$[B]`**
|
|
130
144
|
|
|
131
145
|
```scala
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
Scope.global.scoped { scope =>
|
|
147
|
+
import scope.*
|
|
148
|
+
|
|
149
|
+
val db: $[Database] = Resource.from[Database].allocate
|
|
134
150
|
|
|
135
|
-
//
|
|
136
|
-
val
|
|
151
|
+
val s: String = $(db)(_.query("SELECT 1")) // String is Unscoped => unwrapped
|
|
152
|
+
val n: Int = $(db)(_.query("x").length) // Int is Unscoped => unwrapped
|
|
153
|
+
}
|
|
137
154
|
```
|
|
138
155
|
|
|
139
156
|
---
|
|
140
157
|
|
|
141
158
|
### 3) `Resource[A]`: acquisition + finalization
|
|
142
159
|
|
|
143
|
-
`Resource[A]`
|
|
160
|
+
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).
|
|
144
161
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
162
|
+
#### Constructors
|
|
163
|
+
|
|
164
|
+
From the source:
|
|
165
|
+
|
|
166
|
+
- `Resource(value: => A)`
|
|
167
|
+
Wraps a by-name value; if it's `AutoCloseable`, `close()` is registered automatically (runtime check).
|
|
168
|
+
- `Resource.fromAutoCloseable(thunk: => A <: AutoCloseable)`
|
|
169
|
+
Type-safe helper that registers `close()`.
|
|
170
|
+
- `Resource.acquireRelease(acquire: => A)(release: A => Unit)`
|
|
171
|
+
- `Resource.shared(f: Scope => A)`
|
|
172
|
+
**Memoized + reference-counted**, thread-safe.
|
|
173
|
+
- `Resource.unique(f: Scope => A)`
|
|
174
|
+
Fresh instance per allocation.
|
|
175
|
+
- `Resource.from[T]` and `Resource.from[T](wires*)` (macros)
|
|
176
|
+
Constructor-based dependency injection (covered below).
|
|
148
177
|
|
|
149
|
-
|
|
178
|
+
#### Composition
|
|
150
179
|
|
|
151
|
-
|
|
152
|
-
- Wraps a by-name value; if it's `AutoCloseable`, `close()` is registered automatically.
|
|
153
|
-
- `Resource.acquireRelease(acquire)(release)`
|
|
154
|
-
- Explicit lifecycle.
|
|
155
|
-
- `Resource.fromAutoCloseable(thunk)`
|
|
156
|
-
- A type-safe helper for `AutoCloseable`.
|
|
157
|
-
- `Resource.from[T](wires*)` (macro)
|
|
158
|
-
- The primary entry point for dependency injection.
|
|
159
|
-
- Resolves `T` and all its dependencies into a single `Resource[T]`.
|
|
160
|
-
- Auto-creates missing wires using `Wire.shared` for concrete classes.
|
|
161
|
-
- Requires explicit wires for: primitives, functions, collections, and abstract types.
|
|
162
|
-
- If `T` or any dependency is `AutoCloseable`, registers `close()` automatically.
|
|
180
|
+
`Resource` composes with:
|
|
163
181
|
|
|
164
|
-
|
|
182
|
+
- `map`
|
|
183
|
+
- `flatMap`
|
|
184
|
+
- `zip`
|
|
165
185
|
|
|
166
|
-
|
|
186
|
+
Finalizers remain tied to the allocation scope; in composed resources, finalizers still run LIFO.
|
|
167
187
|
|
|
168
|
-
|
|
169
|
-
- Produces a **fresh** instance every time you allocate it (typical for `Resource(...)`, `acquireRelease`, etc.).
|
|
170
|
-
- `Resource.Shared[A]`
|
|
171
|
-
- Produces a **shared** instance per `Resource.Shared` value, with **reference counting**:
|
|
172
|
-
- the first allocation initializes the value and collects finalizers
|
|
173
|
-
- each allocating scope registers a decrement finalizer
|
|
174
|
-
- when the reference count reaches zero, the collected finalizers run
|
|
188
|
+
#### Sharing vs uniqueness (important)
|
|
175
189
|
|
|
176
|
-
|
|
190
|
+
There are two distinct ideas:
|
|
191
|
+
|
|
192
|
+
1. **Uniqueness**: "each allocation yields a fresh instance"
|
|
193
|
+
- Use `Resource.unique(...)`, or most ordinary `Resource(...)` / `acquireRelease(...)` resources.
|
|
194
|
+
- Each `allocate` runs the acquisition again and registers an independent finalizer.
|
|
195
|
+
|
|
196
|
+
2. **Sharing**: "reusing the same instance across multiple allocations"
|
|
197
|
+
- Use `Resource.shared(...)` (or wires/resources that convert to shared).
|
|
198
|
+
- Sharing is tied to **reusing the same `Resource.Shared` value**, not "magic caching inside a scope".
|
|
199
|
+
- 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.
|
|
177
200
|
|
|
178
201
|
---
|
|
179
202
|
|
|
180
|
-
### 4) `A
|
|
203
|
+
### 4) `Unscoped[A]`: types that may escape a scope
|
|
204
|
+
|
|
205
|
+
`Unscoped[A]` is a marker typeclass for *pure data*. It's used in two places:
|
|
206
|
+
|
|
207
|
+
1. `Scope.scoped` requires `Unscoped[A]` for the block's result type
|
|
208
|
+
⇒ prevents returning resources, closures, or scoped values.
|
|
209
|
+
2. `$` auto-unwraps results of type `B` when `B: Unscoped`.
|
|
210
|
+
|
|
211
|
+
Built-in instances include primitives, `String`, many collections/containers, time values, `java.util.UUID`, and `zio.blocks.chunk.Chunk` (when element types are unscoped).
|
|
181
212
|
|
|
182
|
-
|
|
213
|
+
#### Deriving / defining your own instances
|
|
183
214
|
|
|
184
|
-
|
|
215
|
+
Scala 3 (derivation via `Unscoped.derived`):
|
|
185
216
|
|
|
186
217
|
```scala
|
|
187
|
-
scope
|
|
218
|
+
import zio.blocks.scope.*
|
|
219
|
+
|
|
220
|
+
final case class Config(debug: Boolean)
|
|
221
|
+
object Config:
|
|
222
|
+
given Unscoped[Config] = Unscoped.derived
|
|
188
223
|
```
|
|
189
224
|
|
|
190
|
-
|
|
225
|
+
Scala 2.13:
|
|
191
226
|
|
|
192
|
-
|
|
193
|
-
|
|
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)
|
|
227
|
+
```scala
|
|
228
|
+
import zio.blocks.scope.*
|
|
200
229
|
|
|
201
|
-
|
|
230
|
+
final case class Config(debug: Boolean)
|
|
231
|
+
object Config {
|
|
232
|
+
implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
|
|
233
|
+
}
|
|
234
|
+
```
|
|
202
235
|
|
|
203
|
-
|
|
236
|
+
#### Scope boundary example
|
|
204
237
|
|
|
205
238
|
```scala
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
239
|
+
import zio.blocks.scope.*
|
|
240
|
+
|
|
241
|
+
Scope.global.scoped { parent =>
|
|
242
|
+
import parent.*
|
|
243
|
+
|
|
244
|
+
val ok: String =
|
|
245
|
+
parent.scoped { child =>
|
|
246
|
+
"hello" // String is Unscoped
|
|
247
|
+
}
|
|
212
248
|
|
|
213
|
-
|
|
249
|
+
// Does not compile: returning a resourceful value from a scoped block
|
|
250
|
+
// val leaked: Database =
|
|
251
|
+
// parent.scoped { child =>
|
|
252
|
+
// import child.*
|
|
253
|
+
// Resource.fromAutoCloseable(new Database).allocate
|
|
254
|
+
// }
|
|
255
|
+
|
|
256
|
+
ok
|
|
214
257
|
}
|
|
215
258
|
```
|
|
216
259
|
|
|
217
260
|
---
|
|
218
261
|
|
|
219
|
-
### 5) `
|
|
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.
|
|
262
|
+
### 5) `lower`: using a parent-scoped value in a child scope
|
|
222
263
|
|
|
223
|
-
|
|
224
|
-
- Primitives: `Int`, `Long`, `Boolean`, `Double`, etc.
|
|
225
|
-
- `String`, `Unit`, `Nothing`
|
|
226
|
-
- Collections of Unscoped types
|
|
264
|
+
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:
|
|
227
265
|
|
|
228
|
-
**Custom Unscoped types:**
|
|
229
266
|
```scala
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
267
|
+
import zio.blocks.scope.*
|
|
268
|
+
|
|
269
|
+
Scope.global.scoped { outer =>
|
|
270
|
+
import outer.*
|
|
271
|
+
|
|
272
|
+
val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
273
|
+
|
|
274
|
+
outer.scoped { inner =>
|
|
275
|
+
import inner.*
|
|
276
|
+
val innerDb: $[Database] = lower(db)
|
|
277
|
+
$(innerDb)(_.query("child"))
|
|
278
|
+
}
|
|
233
279
|
}
|
|
234
280
|
```
|
|
235
281
|
|
|
236
|
-
|
|
282
|
+
This is safe because **parents always outlive children** (child finalizers run before the parent closes).
|
|
237
283
|
|
|
238
284
|
---
|
|
239
285
|
|
|
240
|
-
### 6) `
|
|
286
|
+
### 6) `defer`: manual finalizers (+ cancellation)
|
|
241
287
|
|
|
242
|
-
|
|
288
|
+
Use `defer` to register cleanup. It returns a `DeferHandle` you can cancel.
|
|
243
289
|
|
|
244
|
-
|
|
290
|
+
```scala
|
|
291
|
+
import zio.blocks.scope.*
|
|
245
292
|
|
|
246
|
-
|
|
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) |
|
|
293
|
+
Scope.global.scoped { scope =>
|
|
294
|
+
import scope.*
|
|
252
295
|
|
|
253
|
-
|
|
296
|
+
val in = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
|
|
254
297
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
- **`Nothing`**: For blocks that throw
|
|
258
|
-
- **Anything from global scope**: Global scope never closes
|
|
298
|
+
val h: DeferHandle =
|
|
299
|
+
defer(in.close())
|
|
259
300
|
|
|
260
|
-
|
|
301
|
+
val first = in.read()
|
|
302
|
+
println(first)
|
|
261
303
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
304
|
+
// If you already cleaned up manually:
|
|
305
|
+
// h.cancel() // thread-safe, idempotent
|
|
306
|
+
}
|
|
307
|
+
```
|
|
265
308
|
|
|
266
|
-
|
|
267
|
-
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
|
-
}
|
|
309
|
+
There is also a **package-level** helper that only requires a `Finalizer`:
|
|
272
310
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
val same: Database @@ parent.Tag = parent.scoped { _ =>
|
|
276
|
-
parentDb
|
|
277
|
-
}
|
|
311
|
+
```scala
|
|
312
|
+
import zio.blocks.scope.*
|
|
278
313
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// () => println("captured!") // Function types rejected
|
|
283
|
-
// }
|
|
314
|
+
Scope.global.scoped { scope =>
|
|
315
|
+
import scope.*
|
|
316
|
+
given Finalizer = scope
|
|
284
317
|
|
|
285
|
-
//
|
|
286
|
-
// val escaped = parent.scoped { child =>
|
|
287
|
-
// child.allocate(Resource[Database]) // Database @@ child.Tag can't escape
|
|
288
|
-
// }
|
|
318
|
+
defer(println("cleanup")) // uses the package-level helper
|
|
289
319
|
}
|
|
290
320
|
```
|
|
291
321
|
|
|
292
322
|
---
|
|
293
323
|
|
|
294
|
-
### 7) `
|
|
324
|
+
### 7) `open()`: non-lexical, explicitly-managed child scopes
|
|
325
|
+
|
|
326
|
+
`scoped` ties lifetime to a block. `open()` creates a child scope you close explicitly.
|
|
327
|
+
|
|
328
|
+
- The child scope is **unowned** (can be used from any thread)
|
|
329
|
+
- Still **linked to the parent**: parent closing will also close the child
|
|
330
|
+
- You must call `close()` on the handle to detach + finalize now
|
|
295
331
|
|
|
296
|
-
`
|
|
332
|
+
From `Scope.global` the returned type is `Scope.OpenScope` directly (because global `$[A] = A`):
|
|
297
333
|
|
|
298
|
-
|
|
299
|
-
|
|
334
|
+
```scala
|
|
335
|
+
import zio.blocks.scope.*
|
|
336
|
+
|
|
337
|
+
val os: Scope.OpenScope = Scope.global.open()
|
|
338
|
+
|
|
339
|
+
val db = os.scope.allocate(Resource.fromAutoCloseable(new Database))
|
|
300
340
|
|
|
301
|
-
|
|
341
|
+
// ... use db ...
|
|
342
|
+
|
|
343
|
+
os.close().orThrow()
|
|
344
|
+
```
|
|
302
345
|
|
|
303
|
-
|
|
304
|
-
- `Wire.Unique`: produces a fresh instance each time
|
|
346
|
+
Inside a child scope, `open()` returns `$[Scope.OpenScope]`. Prefer using it safely via `$`:
|
|
305
347
|
|
|
306
|
-
|
|
348
|
+
```scala
|
|
349
|
+
import zio.blocks.scope.*
|
|
307
350
|
|
|
308
|
-
|
|
351
|
+
Scope.global.scoped { parent =>
|
|
352
|
+
import parent.*
|
|
309
353
|
|
|
310
|
-
|
|
354
|
+
val os: $[Scope.OpenScope] = open()
|
|
311
355
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
| `Wire.unique[T]` | Create a unique wire from `T`'s constructor |
|
|
316
|
-
| `Resource.from[T](wires*)` | Wire up `T` and all dependencies into a `Resource` |
|
|
356
|
+
$(os) { h =>
|
|
357
|
+
val child = h.scope
|
|
358
|
+
val db = child.allocate(Resource.fromAutoCloseable(new Database))
|
|
317
359
|
|
|
318
|
-
|
|
360
|
+
// ...
|
|
361
|
+
h.close().orThrow()
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
319
365
|
|
|
320
|
-
|
|
366
|
+
---
|
|
321
367
|
|
|
322
|
-
|
|
368
|
+
### 8) Escape hatch: `leak`
|
|
323
369
|
|
|
324
|
-
|
|
325
|
-
2. **Validate**: Checks for cycles, unmakeable types, duplicate providers
|
|
326
|
-
3. **Topological sort**: Orders dependencies so leaves are allocated first
|
|
327
|
-
4. **Generate composition**: Produces a `Resource[T]` via flatMap chains
|
|
370
|
+
Sometimes you must hand a raw value to code that cannot work with `$[A]`. Use `leak`:
|
|
328
371
|
|
|
329
|
-
|
|
372
|
+
```scala
|
|
373
|
+
import zio.blocks.scope.*
|
|
330
374
|
|
|
331
|
-
|
|
332
|
-
|
|
375
|
+
Scope.global.scoped { scope =>
|
|
376
|
+
import scope.*
|
|
333
377
|
|
|
334
|
-
|
|
378
|
+
val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
335
379
|
|
|
336
|
-
|
|
380
|
+
val raw: Database = leak(db) // emits a compiler warning
|
|
381
|
+
// thirdParty(raw)
|
|
382
|
+
}
|
|
383
|
+
```
|
|
337
384
|
|
|
338
|
-
|
|
385
|
+
`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.
|
|
339
386
|
|
|
340
387
|
---
|
|
341
388
|
|
|
342
389
|
## Safety model (why leaking is prevented)
|
|
343
390
|
|
|
344
|
-
|
|
391
|
+
Scope's safety comes from *three reinforcing layers*.
|
|
392
|
+
|
|
393
|
+
### 1) Type barrier: scope-specific `$[A]`
|
|
394
|
+
|
|
395
|
+
Every scope has a distinct `$[A]` type. You cannot accidentally use values across scopes without an explicit conversion (`lower` for parent → child).
|
|
396
|
+
|
|
397
|
+
### 2) Controlled access: `$` macro restricts lambda usage
|
|
345
398
|
|
|
346
|
-
The
|
|
399
|
+
The `$` operator only allows using the unwrapped value as a **method/field receiver**. This prevents:
|
|
347
400
|
|
|
348
|
-
|
|
401
|
+
- returning the resource
|
|
402
|
+
- storing it in a local val/var
|
|
403
|
+
- passing it as an argument
|
|
404
|
+
- capturing it in a closure
|
|
349
405
|
|
|
350
|
-
|
|
406
|
+
Also note: `$` requires a **lambda literal**. Method references / variables are rejected:
|
|
351
407
|
|
|
352
408
|
```scala
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
}
|
|
409
|
+
// does not compile:
|
|
410
|
+
val f: Database => String = _.query("x")
|
|
411
|
+
(scope $ db)(f) // "$ requires a lambda literal ..."
|
|
358
412
|
```
|
|
359
413
|
|
|
360
|
-
|
|
414
|
+
### 3) Scope boundary rule: `scoped` requires `Unscoped[A]`
|
|
415
|
+
|
|
416
|
+
A `scoped { ... }` block can only return pure data (or `Nothing`). Resources and closures cannot escape.
|
|
417
|
+
|
|
418
|
+
**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`).
|
|
361
419
|
|
|
362
|
-
|
|
363
|
-
`ScopeCompileTimeSafetyScala3Spec`.
|
|
420
|
+
### Closed-scope defense (runtime)
|
|
364
421
|
|
|
365
|
-
|
|
422
|
+
If a scope escapes and is used after closing, operations become **no-ops returning default values**:
|
|
366
423
|
|
|
367
|
-
|
|
424
|
+
- `$`, `allocate`, `open`, `lower` return defaults (`null`, `0`, `false`, …)
|
|
425
|
+
- `$` does not run the lambda when closed
|
|
426
|
+
- `defer` on a closed scope is ignored
|
|
427
|
+
- `scoped` creates a born-closed child if the parent is already closed
|
|
368
428
|
|
|
369
|
-
|
|
429
|
+
This prevents post-close interaction with released resources, but can produce surprising default values if scopes are misused across threads.
|
|
430
|
+
|
|
431
|
+
### Thread ownership rule (JVM)
|
|
432
|
+
|
|
433
|
+
- Scopes created by `scoped` are **owned** by the entering thread.
|
|
434
|
+
- Calling `scoped` on a scope you don't own throws `IllegalStateException`.
|
|
435
|
+
- `open()` creates an **unowned** child scope (`isOwner == true` from any thread).
|
|
436
|
+
|
|
437
|
+
(Scala.js uses a trivial ownership model; `isOwner` is effectively always true.)
|
|
370
438
|
|
|
371
439
|
---
|
|
372
440
|
|
|
373
|
-
## Usage examples
|
|
441
|
+
## Usage examples (patterns)
|
|
374
442
|
|
|
375
443
|
### Allocating and using a resource
|
|
376
444
|
|
|
377
445
|
```scala
|
|
378
|
-
import zio.blocks.scope
|
|
446
|
+
import zio.blocks.scope.*
|
|
379
447
|
|
|
380
|
-
final class FileHandle(path: String) extends AutoCloseable
|
|
448
|
+
final class FileHandle(path: String) extends AutoCloseable:
|
|
381
449
|
def readAll(): String = s"contents of $path"
|
|
382
450
|
def close(): Unit = println(s"closed $path")
|
|
383
|
-
}
|
|
384
451
|
|
|
385
|
-
|
|
386
|
-
|
|
452
|
+
@main def fileExample(): Unit =
|
|
453
|
+
Scope.global.scoped { scope =>
|
|
454
|
+
import scope.*
|
|
455
|
+
|
|
456
|
+
val h: $[FileHandle] =
|
|
457
|
+
Resource(new FileHandle("data.txt")).allocate
|
|
458
|
+
|
|
459
|
+
val contents: String =
|
|
460
|
+
$(h)(_.readAll())
|
|
387
461
|
|
|
388
|
-
// $ executes immediately - work with the value inside the function
|
|
389
|
-
(scope $ h) { handle =>
|
|
390
|
-
val contents = handle.readAll()
|
|
391
462
|
println(contents)
|
|
392
463
|
}
|
|
393
|
-
}
|
|
394
464
|
```
|
|
395
465
|
|
|
396
466
|
---
|
|
@@ -398,333 +468,393 @@ Scope.global.scoped { scope =>
|
|
|
398
468
|
### Nested scopes (child can use parent, not vice versa)
|
|
399
469
|
|
|
400
470
|
```scala
|
|
401
|
-
import zio.blocks.scope
|
|
471
|
+
import zio.blocks.scope.*
|
|
402
472
|
|
|
403
|
-
|
|
404
|
-
|
|
473
|
+
final class Database extends AutoCloseable:
|
|
474
|
+
def query(sql: String): String = s"result: $sql"
|
|
475
|
+
def close(): Unit = println("db closed")
|
|
405
476
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
println(db.query("SELECT 1"))
|
|
410
|
-
}
|
|
477
|
+
@main def nested(): Unit =
|
|
478
|
+
Scope.global.scoped { parent =>
|
|
479
|
+
import parent.*
|
|
411
480
|
|
|
412
|
-
val
|
|
481
|
+
val parentDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
413
482
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
483
|
+
val done: String =
|
|
484
|
+
parent.scoped { child =>
|
|
485
|
+
import child.*
|
|
418
486
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
// But you cannot return childDb to the parent:
|
|
423
|
-
// childDb : Database @@ child.Tag has no ScopeLift instance
|
|
424
|
-
}
|
|
487
|
+
val db: $[Database] = lower(parentDb)
|
|
488
|
+
println($(db)(_.query("SELECT 1")))
|
|
425
489
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
println(db.query("SELECT 3"))
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
---
|
|
434
|
-
|
|
435
|
-
### Building a `Scoped` program (map/flatMap)
|
|
490
|
+
val childDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
491
|
+
println($(childDb)(_.query("SELECT 2")))
|
|
436
492
|
|
|
437
|
-
|
|
493
|
+
// childDb cannot be returned to the parent (not Unscoped)
|
|
494
|
+
"done"
|
|
495
|
+
}
|
|
438
496
|
|
|
439
|
-
|
|
440
|
-
|
|
497
|
+
println($(parentDb)(_.query("SELECT 3")))
|
|
498
|
+
done
|
|
499
|
+
}
|
|
500
|
+
```
|
|
441
501
|
|
|
442
|
-
|
|
443
|
-
val db: Database @@ scope.Tag = scope.allocate(Resource(new Database))
|
|
502
|
+
Finalizers run **child first, then parent**.
|
|
444
503
|
|
|
445
|
-
|
|
446
|
-
val program: String @@ scope.Tag = db.map { d =>
|
|
447
|
-
d.query("SELECT 1")
|
|
448
|
-
}
|
|
504
|
+
---
|
|
449
505
|
|
|
450
|
-
|
|
451
|
-
// Use side effects inside the computation to observe results
|
|
452
|
-
scope.execute(program)
|
|
453
|
-
}
|
|
454
|
-
```
|
|
506
|
+
### Chaining resource acquisition (`$[Resource[A]]` + `.allocate`)
|
|
455
507
|
|
|
456
|
-
|
|
508
|
+
If a method returns `Resource[A]`, `$` returns a **scoped** `Resource[A]` (because `Resource[A]` is not `Unscoped`). Allocate it without leaking:
|
|
457
509
|
|
|
458
510
|
```scala
|
|
459
|
-
import zio.blocks.scope
|
|
511
|
+
import zio.blocks.scope.*
|
|
460
512
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
def lease: Resource[Connection] = Resource(new Connection)
|
|
513
|
+
final class Pool extends AutoCloseable:
|
|
514
|
+
def lease(): Resource[Conn] = Resource.fromAutoCloseable(new Conn)
|
|
464
515
|
def close(): Unit = println("pool closed")
|
|
465
|
-
}
|
|
466
516
|
|
|
467
|
-
class
|
|
517
|
+
final class Conn extends AutoCloseable:
|
|
468
518
|
def query(sql: String): String = s"result: $sql"
|
|
469
519
|
def close(): Unit = println("connection closed")
|
|
470
|
-
}
|
|
471
520
|
|
|
472
|
-
|
|
473
|
-
|
|
521
|
+
@main def chaining(): Unit =
|
|
522
|
+
Scope.global.scoped { scope =>
|
|
523
|
+
import scope.*
|
|
474
524
|
|
|
475
|
-
|
|
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")
|
|
525
|
+
val pool: $[Pool] = Resource.fromAutoCloseable(new Pool).allocate
|
|
480
526
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
527
|
+
// $(pool)(_.lease()) : $[Resource[Conn]]
|
|
528
|
+
val conn: $[Conn] =
|
|
529
|
+
$(pool)(_.lease()).allocate
|
|
530
|
+
|
|
531
|
+
val result: String =
|
|
532
|
+
$(conn)(_.query("SELECT 1"))
|
|
533
|
+
|
|
534
|
+
println(result)
|
|
535
|
+
}
|
|
485
536
|
```
|
|
486
537
|
|
|
538
|
+
This `.allocate` comes from `Scope.ScopedResourceOps` (an extension on `$[Resource[A]]`).
|
|
539
|
+
|
|
487
540
|
---
|
|
488
541
|
|
|
489
|
-
###
|
|
542
|
+
### Allocating a bare `Resource[A]` with `.allocate`
|
|
490
543
|
|
|
491
|
-
|
|
544
|
+
A plain `Resource[A]` also has `.allocate` as syntax sugar for `scope.allocate(resource)`:
|
|
492
545
|
|
|
493
546
|
```scala
|
|
494
|
-
import zio.blocks.scope
|
|
547
|
+
import zio.blocks.scope.*
|
|
495
548
|
|
|
496
549
|
Scope.global.scoped { scope =>
|
|
497
|
-
|
|
550
|
+
import scope.*
|
|
498
551
|
|
|
499
|
-
|
|
552
|
+
val db: $[Database] =
|
|
553
|
+
Resource.fromAutoCloseable(new Database).allocate
|
|
500
554
|
|
|
501
|
-
|
|
502
|
-
println(firstByte)
|
|
555
|
+
$(db)(_.query("SELECT 1"))
|
|
503
556
|
}
|
|
504
557
|
```
|
|
505
558
|
|
|
506
|
-
|
|
559
|
+
---
|
|
560
|
+
|
|
561
|
+
### Classes with `Finalizer` parameters (cleanup-only capability)
|
|
562
|
+
|
|
563
|
+
If a class only needs cleanup registration, accept a `Finalizer`. DI macros inject it automatically.
|
|
507
564
|
|
|
508
565
|
```scala
|
|
509
|
-
import zio.blocks.scope
|
|
566
|
+
import zio.blocks.scope.*
|
|
510
567
|
|
|
511
|
-
|
|
512
|
-
|
|
568
|
+
final case class Config(url: String)
|
|
569
|
+
object Config:
|
|
570
|
+
given Unscoped[Config] = Unscoped.derived
|
|
513
571
|
|
|
514
|
-
|
|
515
|
-
}
|
|
572
|
+
final class ConnectionPool(config: Config)(using Finalizer):
|
|
573
|
+
private val pool = s"pool(${config.url})"
|
|
574
|
+
defer(println(s"shutdown $pool"))
|
|
575
|
+
|
|
576
|
+
val poolResource: Resource[ConnectionPool] =
|
|
577
|
+
Resource.from[ConnectionPool](
|
|
578
|
+
Wire(Config("jdbc://localhost"))
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
@main def finalizerInjection(): Unit =
|
|
582
|
+
Scope.global.scoped { scope =>
|
|
583
|
+
import scope.*
|
|
584
|
+
val pool: $[ConnectionPool] = poolResource.allocate
|
|
585
|
+
()
|
|
586
|
+
}
|
|
516
587
|
```
|
|
517
588
|
|
|
589
|
+
When to prefer `Finalizer` over `Scope`:
|
|
590
|
+
|
|
591
|
+
- you only need `defer`
|
|
592
|
+
- you want to expose minimal power to the class
|
|
593
|
+
|
|
518
594
|
---
|
|
519
595
|
|
|
520
|
-
### Classes with `
|
|
596
|
+
### Classes with `Scope` parameters (scope injection)
|
|
521
597
|
|
|
522
|
-
If
|
|
598
|
+
If a class needs to allocate resources or create child scopes, accept a `Scope`:
|
|
523
599
|
|
|
524
600
|
```scala
|
|
525
|
-
import zio.blocks.scope
|
|
601
|
+
import zio.blocks.scope.*
|
|
526
602
|
|
|
527
|
-
class
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
def getConnection(): Connection = pool.acquire()
|
|
532
|
-
}
|
|
603
|
+
final case class Config(url: String)
|
|
604
|
+
object Config:
|
|
605
|
+
given Unscoped[Config] = Unscoped.derived
|
|
533
606
|
|
|
534
|
-
|
|
535
|
-
|
|
607
|
+
final class Connection(config: Config) extends AutoCloseable:
|
|
608
|
+
def query(sql: String): String = s"[${config.url}] $sql"
|
|
609
|
+
def close(): Unit = println("connection closed")
|
|
536
610
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
611
|
+
final class RequestHandler(config: Config)(using scope: Scope):
|
|
612
|
+
def handle(sql: String): String =
|
|
613
|
+
scope.scoped { child =>
|
|
614
|
+
import child.*
|
|
615
|
+
val conn: $[Connection] = Resource.fromAutoCloseable(new Connection(config)).allocate
|
|
616
|
+
$(conn)(_.query(sql))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
val handlerResource: Resource[RequestHandler] =
|
|
620
|
+
Resource.from[RequestHandler](
|
|
621
|
+
Wire(Config("jdbc://localhost"))
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
@main def scopeInjection(): Unit =
|
|
625
|
+
Scope.global.scoped { scope =>
|
|
626
|
+
import scope.*
|
|
627
|
+
val handler: $[RequestHandler] = handlerResource.allocate
|
|
628
|
+
val out: String = $(handler)(_.handle("SELECT 1"))
|
|
629
|
+
println(out)
|
|
630
|
+
}
|
|
541
631
|
```
|
|
542
632
|
|
|
543
|
-
|
|
544
|
-
- `Finalizer` is the minimal interface—it only has `defer`
|
|
545
|
-
- Classes that need cleanup should not have access to `allocate` or `$`
|
|
546
|
-
- The macros pass a `Finalizer` at runtime, so declaring `Scope` would be misleading
|
|
633
|
+
The `Scope`/`Finalizer` parameter can appear in any parameter list position; it's recognized specially by the derivation macros.
|
|
547
634
|
|
|
548
635
|
---
|
|
549
636
|
|
|
550
|
-
|
|
637
|
+
## Dependency injection (DI) with `Wire` + `Resource.from`
|
|
638
|
+
|
|
639
|
+
Scope includes a small constructor-based DI layer built on top of `zio.blocks.context.Context`.
|
|
640
|
+
|
|
641
|
+
### `Wire[-In, +Out]`: a dependency recipe
|
|
551
642
|
|
|
552
|
-
|
|
643
|
+
A `Wire` is a recipe for constructing `Out` from a `Context[In]` (and a `Scope` for finalization):
|
|
644
|
+
|
|
645
|
+
- `Wire.Shared` → converts to `Resource.shared` (ref-counted sharing)
|
|
646
|
+
- `Wire.Unique` → converts to `Resource.unique` (fresh instance)
|
|
647
|
+
|
|
648
|
+
#### Manual wire + Context
|
|
553
649
|
|
|
554
650
|
```scala
|
|
555
|
-
import zio.blocks.scope
|
|
651
|
+
import zio.blocks.scope.*
|
|
556
652
|
import zio.blocks.context.Context
|
|
557
653
|
|
|
558
654
|
final case class Config(debug: Boolean)
|
|
655
|
+
object Config:
|
|
656
|
+
given Unscoped[Config] = Unscoped.derived
|
|
559
657
|
|
|
560
|
-
val w: Wire.Shared[Boolean, Config] =
|
|
561
|
-
|
|
658
|
+
val w: Wire.Shared[Boolean, Config] =
|
|
659
|
+
Wire.shared[Config] // Boolean => Config
|
|
562
660
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
scope.allocate(w.toResource(deps))
|
|
661
|
+
val deps: Context[Boolean] =
|
|
662
|
+
Context(true)
|
|
566
663
|
|
|
567
|
-
|
|
568
|
-
|
|
664
|
+
@main def wireAndContext(): Unit =
|
|
665
|
+
Scope.global.scoped { scope =>
|
|
666
|
+
import scope.*
|
|
569
667
|
|
|
570
|
-
|
|
571
|
-
|
|
668
|
+
val cfg: $[Config] =
|
|
669
|
+
allocate(w.toResource(deps))
|
|
670
|
+
|
|
671
|
+
val debug: Boolean =
|
|
672
|
+
$(cfg)(_.debug)
|
|
673
|
+
|
|
674
|
+
println(debug)
|
|
675
|
+
}
|
|
572
676
|
```
|
|
573
677
|
|
|
574
|
-
Sharing vs uniqueness at the wire level
|
|
678
|
+
#### Sharing vs uniqueness at the wire level
|
|
575
679
|
|
|
576
680
|
```scala
|
|
577
|
-
import zio.blocks.scope
|
|
681
|
+
import zio.blocks.scope.*
|
|
578
682
|
|
|
579
|
-
val ws = Wire.shared[Config] // shared recipe
|
|
580
|
-
val wu = Wire.unique[Config] // unique recipe
|
|
683
|
+
val ws = Wire.shared[Config] // shared recipe
|
|
684
|
+
val wu = Wire.unique[Config] // unique recipe
|
|
581
685
|
```
|
|
582
686
|
|
|
687
|
+
The difference is realized when converting to resources (`toResource`) and allocating.
|
|
688
|
+
|
|
583
689
|
---
|
|
584
690
|
|
|
585
|
-
###
|
|
691
|
+
### `Resource.from[T](wires*)`: derive a whole object graph
|
|
692
|
+
|
|
693
|
+
`Resource.from[T](wires*)` is the primary entry point for DI. It:
|
|
586
694
|
|
|
587
|
-
|
|
695
|
+
- uses provided wires as overrides
|
|
696
|
+
- auto-creates missing wires for **concrete classes** (defaulting to shared)
|
|
697
|
+
- rejects unmakeable/abstract types unless you provide a wire
|
|
698
|
+
- detects cycles, duplicate providers, and subtype conflicts
|
|
699
|
+
- generates a composed `Resource[T]` via `flatMap` chains (preserving sharing/uniqueness)
|
|
700
|
+
|
|
701
|
+
Example:
|
|
588
702
|
|
|
589
703
|
```scala
|
|
590
|
-
import zio.blocks.scope
|
|
704
|
+
import zio.blocks.scope.*
|
|
591
705
|
|
|
592
706
|
final case class Config(url: String)
|
|
707
|
+
object Config:
|
|
708
|
+
given Unscoped[Config] = Unscoped.derived
|
|
593
709
|
|
|
594
|
-
final class Logger
|
|
710
|
+
final class Logger:
|
|
595
711
|
def info(msg: String): Unit = println(msg)
|
|
596
|
-
}
|
|
597
712
|
|
|
598
|
-
final class Database(cfg: Config) extends AutoCloseable
|
|
713
|
+
final class Database(cfg: Config) extends AutoCloseable:
|
|
599
714
|
def query(sql: String): String = s"[${cfg.url}] $sql"
|
|
600
715
|
def close(): Unit = println("database closed")
|
|
601
|
-
}
|
|
602
716
|
|
|
603
|
-
final class Service(db: Database, logger: Logger) extends AutoCloseable
|
|
717
|
+
final class Service(db: Database, logger: Logger) extends AutoCloseable:
|
|
604
718
|
def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
|
|
605
719
|
def close(): Unit = println("service closed")
|
|
606
|
-
}
|
|
607
720
|
|
|
608
|
-
// Only provide leaf values (primitives, configs) - the rest is auto-wired:
|
|
609
721
|
val serviceResource: Resource[Service] =
|
|
610
722
|
Resource.from[Service](
|
|
611
|
-
Wire(Config("jdbc:postgresql://localhost/db"))
|
|
723
|
+
Wire(Config("jdbc:postgresql://localhost/db")) // leaf value
|
|
612
724
|
)
|
|
613
725
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
726
|
+
@main def di(): Unit =
|
|
727
|
+
Scope.global.scoped { scope =>
|
|
728
|
+
import scope.*
|
|
729
|
+
val svc: $[Service] = serviceResource.allocate
|
|
730
|
+
$(svc)(_.run())
|
|
731
|
+
}
|
|
620
732
|
```
|
|
621
733
|
|
|
622
|
-
|
|
623
|
-
- Leaf values: primitives, configs, pre-existing instances via `Wire(value)`
|
|
624
|
-
- Abstract types: traits/abstract classes via `Wire.shared[ConcreteImpl]`
|
|
625
|
-
- Overrides: when you want `unique` instead of the default `shared`
|
|
626
|
-
|
|
627
|
-
**What is auto-created:**
|
|
628
|
-
- Concrete classes with accessible primary constructors (default: `Wire.shared`)
|
|
629
|
-
|
|
630
|
-
---
|
|
631
|
-
|
|
632
|
-
### Injecting traits via subtype wires
|
|
734
|
+
#### Injecting traits via subtype wires
|
|
633
735
|
|
|
634
|
-
When a dependency is
|
|
736
|
+
When a dependency is abstract, provide a wire for a concrete implementation:
|
|
635
737
|
|
|
636
738
|
```scala
|
|
637
|
-
import zio.blocks.scope
|
|
739
|
+
import zio.blocks.scope.*
|
|
638
740
|
|
|
639
|
-
trait Logger
|
|
741
|
+
trait Logger:
|
|
640
742
|
def info(msg: String): Unit
|
|
641
|
-
}
|
|
642
743
|
|
|
643
|
-
final class ConsoleLogger extends Logger
|
|
744
|
+
final class ConsoleLogger extends Logger:
|
|
644
745
|
def info(msg: String): Unit = println(msg)
|
|
645
|
-
}
|
|
646
746
|
|
|
647
|
-
final class App(logger: Logger)
|
|
747
|
+
final class App(logger: Logger):
|
|
648
748
|
def run(): Unit = logger.info("Hello!")
|
|
649
|
-
}
|
|
650
749
|
|
|
651
|
-
// Wire.shared[ConsoleLogger] satisfies the Logger dependency via subtyping:
|
|
652
750
|
val appResource: Resource[App] =
|
|
653
751
|
Resource.from[App](
|
|
654
|
-
Wire.shared[ConsoleLogger]
|
|
752
|
+
Wire.shared[ConsoleLogger] // satisfies Logger via subtyping
|
|
655
753
|
)
|
|
656
754
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
755
|
+
@main def traitInjection(): Unit =
|
|
756
|
+
Scope.global.scoped { scope =>
|
|
757
|
+
import scope.*
|
|
758
|
+
val app: $[App] = appResource.allocate
|
|
759
|
+
$(app)(_.run())
|
|
760
|
+
}
|
|
661
761
|
```
|
|
662
762
|
|
|
663
|
-
|
|
763
|
+
#### Diamond patterns share a single instance (when appropriate)
|
|
664
764
|
|
|
665
765
|
```scala
|
|
766
|
+
import zio.blocks.scope.*
|
|
767
|
+
|
|
666
768
|
trait Service
|
|
667
|
-
class LiveService extends Service
|
|
668
|
-
class NeedsService(s: Service)
|
|
669
|
-
class NeedsLive(l: LiveService)
|
|
670
|
-
class App(a: NeedsService, b: NeedsLive)
|
|
769
|
+
final class LiveService extends Service
|
|
770
|
+
final class NeedsService(s: Service)
|
|
771
|
+
final class NeedsLive(l: LiveService)
|
|
772
|
+
final class App(a: NeedsService, b: NeedsLive)
|
|
671
773
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
)
|
|
676
|
-
//
|
|
774
|
+
val appResource: Resource[App] =
|
|
775
|
+
Resource.from[App](
|
|
776
|
+
Wire.shared[LiveService]
|
|
777
|
+
)
|
|
778
|
+
// LiveService instantiations: 1
|
|
677
779
|
```
|
|
678
780
|
|
|
679
781
|
---
|
|
680
782
|
|
|
681
|
-
|
|
783
|
+
## Common compile errors (and what they mean)
|
|
682
784
|
|
|
683
|
-
|
|
785
|
+
This module produces two kinds of compile-time feedback:
|
|
684
786
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
Scope.global.scoped { scope =>
|
|
689
|
-
val db = scope.allocate(Resource(new Database))
|
|
787
|
+
- **Plain macro aborts** for unsafe `$` usage
|
|
788
|
+
- **ASCII-rendered** errors/warnings for DI derivation + leak warnings (via `internal.ErrorMessages`)
|
|
690
789
|
|
|
691
|
-
|
|
692
|
-
// thirdParty(raw)
|
|
693
|
-
}
|
|
694
|
-
```
|
|
790
|
+
### Unsafe use inside `$`
|
|
695
791
|
|
|
696
|
-
|
|
792
|
+
Typical messages include:
|
|
697
793
|
|
|
698
|
-
|
|
794
|
+
```
|
|
795
|
+
Unsafe use of scoped value: the lambda parameter cannot be passed as an argument to a function or method.
|
|
796
|
+
```
|
|
699
797
|
|
|
700
|
-
|
|
798
|
+
Other variants:
|
|
701
799
|
|
|
702
|
-
|
|
800
|
+
- `Unsafe use of scoped value: the lambda parameter cannot be captured in a nested lambda or closure.`
|
|
801
|
+
- `Unsafe use of scoped value: the lambda parameter must only be used as a method receiver ...`
|
|
802
|
+
- `$ requires a lambda literal: (scope $ x)(a => a.method()). Method references and variables are not supported.`
|
|
703
803
|
|
|
704
|
-
### Not a class (Wire.shared/unique on trait
|
|
804
|
+
### Not a class (`Wire.shared/unique` on a trait / abstract)
|
|
705
805
|
|
|
706
806
|
```
|
|
707
|
-
── Scope Error
|
|
807
|
+
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
708
808
|
|
|
709
809
|
Cannot derive Wire for MyTrait: not a class.
|
|
710
810
|
|
|
711
811
|
Hint: Use Wire.Shared / Wire.Unique directly.
|
|
712
812
|
|
|
713
|
-
|
|
813
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### No primary constructor
|
|
817
|
+
|
|
818
|
+
```
|
|
819
|
+
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
MyType has no primary constructor.
|
|
822
|
+
|
|
823
|
+
Hint: Use Wire.Shared / Wire.Unique directly
|
|
824
|
+
with a custom construction strategy.
|
|
825
|
+
|
|
826
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
### `Resource.from[T]` used when `T` has dependencies
|
|
830
|
+
|
|
831
|
+
```
|
|
832
|
+
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
833
|
+
|
|
834
|
+
Resource.from[MyService] cannot be derived.
|
|
835
|
+
|
|
836
|
+
MyService has dependencies that must be provided:
|
|
837
|
+
• Config
|
|
838
|
+
• Logger
|
|
839
|
+
|
|
840
|
+
Hint: Use Resource.from[MyService](wire1, wire2, ...)
|
|
841
|
+
to provide wires for all dependencies.
|
|
842
|
+
|
|
843
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
714
844
|
```
|
|
715
845
|
|
|
716
846
|
### Unmakeable type (primitives, functions, collections)
|
|
717
847
|
|
|
718
848
|
```
|
|
719
|
-
── Scope Error
|
|
849
|
+
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
720
850
|
|
|
721
851
|
Cannot auto-create String
|
|
722
852
|
|
|
723
853
|
This type (primitive, collection, or function) cannot be auto-created.
|
|
724
854
|
|
|
725
855
|
Required by:
|
|
726
|
-
|
|
727
|
-
|
|
856
|
+
├── Config
|
|
857
|
+
└── App
|
|
728
858
|
|
|
729
859
|
Fix: Provide Wire(value) with the desired value:
|
|
730
860
|
|
|
@@ -733,20 +863,20 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
733
863
|
...
|
|
734
864
|
)
|
|
735
865
|
|
|
736
|
-
|
|
866
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
737
867
|
```
|
|
738
868
|
|
|
739
|
-
### Abstract type (trait
|
|
869
|
+
### Abstract type (trait / abstract class dependency)
|
|
740
870
|
|
|
741
871
|
```
|
|
742
|
-
── Scope Error
|
|
872
|
+
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
743
873
|
|
|
744
874
|
Cannot auto-create Logger
|
|
745
875
|
|
|
746
876
|
This type is abstract (trait or abstract class).
|
|
747
877
|
|
|
748
878
|
Required by:
|
|
749
|
-
|
|
879
|
+
└── App
|
|
750
880
|
|
|
751
881
|
Fix: Provide a wire for a concrete implementation:
|
|
752
882
|
|
|
@@ -755,13 +885,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
755
885
|
...
|
|
756
886
|
)
|
|
757
887
|
|
|
758
|
-
|
|
888
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
759
889
|
```
|
|
760
890
|
|
|
761
891
|
### Duplicate providers (ambiguous wires)
|
|
762
892
|
|
|
763
893
|
```
|
|
764
|
-
── Scope Error
|
|
894
|
+
── Scope Error ────────────────────────────────────────────────────────────────
|
|
765
895
|
|
|
766
896
|
Multiple providers for Service
|
|
767
897
|
|
|
@@ -771,13 +901,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
771
901
|
|
|
772
902
|
Hint: Remove duplicate wires or use distinct wrapper types.
|
|
773
903
|
|
|
774
|
-
|
|
904
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
775
905
|
```
|
|
776
906
|
|
|
777
907
|
### Dependency cycle
|
|
778
908
|
|
|
779
909
|
```
|
|
780
|
-
── Scope Error
|
|
910
|
+
── Scope Error ────────────────────────────────────────────────────────────────
|
|
781
911
|
|
|
782
912
|
Dependency cycle detected
|
|
783
913
|
|
|
@@ -793,13 +923,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
793
923
|
• Using lazy initialization
|
|
794
924
|
• Restructuring dependencies
|
|
795
925
|
|
|
796
|
-
|
|
926
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
797
927
|
```
|
|
798
928
|
|
|
799
929
|
### Subtype conflict (related dependency types)
|
|
800
930
|
|
|
801
931
|
```
|
|
802
|
-
── Scope Error
|
|
932
|
+
── Scope Error ────────────────────────────────────────────────────────────────
|
|
803
933
|
|
|
804
934
|
Dependency type conflict in MyService
|
|
805
935
|
|
|
@@ -812,16 +942,16 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
812
942
|
To fix this, wrap one or both types in a distinct wrapper:
|
|
813
943
|
|
|
814
944
|
case class WrappedInputStream(value: InputStream)
|
|
815
|
-
|
|
945
|
+
or
|
|
816
946
|
opaque type WrappedInputStream = InputStream
|
|
817
947
|
|
|
818
|
-
|
|
948
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
819
949
|
```
|
|
820
950
|
|
|
821
|
-
### Duplicate parameter types in constructor
|
|
951
|
+
### Duplicate parameter types in a constructor
|
|
822
952
|
|
|
823
953
|
```
|
|
824
|
-
── Scope Error
|
|
954
|
+
── Scope Error ────────────────────────────────────────────────────────────────
|
|
825
955
|
|
|
826
956
|
Constructor of App has multiple parameters of type String
|
|
827
957
|
|
|
@@ -830,118 +960,240 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
830
960
|
Fix: Wrap one parameter in an opaque type to distinguish them:
|
|
831
961
|
|
|
832
962
|
opaque type FirstString = String
|
|
833
|
-
|
|
963
|
+
or
|
|
834
964
|
case class FirstString(value: String)
|
|
835
965
|
|
|
836
|
-
|
|
966
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
837
967
|
```
|
|
838
968
|
|
|
839
969
|
### Leak warning
|
|
840
970
|
|
|
841
|
-
When using `leak(value)` to escape the scoped type system:
|
|
842
|
-
|
|
843
971
|
```
|
|
844
|
-
── Scope Warning
|
|
972
|
+
── Scope Warning ───────────────────────────────────────────────────────────────
|
|
845
973
|
|
|
846
974
|
leak(db)
|
|
847
975
|
^
|
|
848
976
|
|
|
|
849
977
|
|
|
850
|
-
Warning: db is being leaked from scope
|
|
978
|
+
Warning: db is being leaked from scope zio.blocks.scope.Scope.Child[...].
|
|
851
979
|
This may result in undefined behavior.
|
|
852
980
|
|
|
853
981
|
Hint:
|
|
854
982
|
If you know this data type is not resourceful, then add an Unscoped
|
|
855
|
-
instance for it so
|
|
983
|
+
instance for it so you do not need to leak it.
|
|
856
984
|
|
|
857
|
-
|
|
985
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
858
986
|
```
|
|
859
987
|
|
|
860
988
|
---
|
|
861
989
|
|
|
862
|
-
## API reference (
|
|
990
|
+
## API reference (from source)
|
|
991
|
+
|
|
992
|
+
Examples below use Scala 3 syntax. Scala 2.13 has equivalent APIs, but macro signatures differ slightly (notably `$`'s return type encoding).
|
|
863
993
|
|
|
864
994
|
### `Scope`
|
|
865
995
|
|
|
866
|
-
|
|
996
|
+
```scala
|
|
997
|
+
sealed abstract class Scope extends Finalizer with ScopeVersionSpecific
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
Associated types and hierarchy:
|
|
1001
|
+
|
|
1002
|
+
- `type $[+A]`
|
|
1003
|
+
- `type Parent <: Scope`
|
|
1004
|
+
- `val parent: Parent`
|
|
1005
|
+
- `def isClosed: Boolean`
|
|
1006
|
+
- `def isOwner: Boolean`
|
|
1007
|
+
|
|
1008
|
+
Core operations:
|
|
867
1009
|
|
|
868
1010
|
```scala
|
|
869
|
-
|
|
870
|
-
type Tag = Tag0
|
|
1011
|
+
def scoped[A](f: (child: Scope.Child[this.type]) => A)(using Unscoped[A]): A
|
|
871
1012
|
|
|
872
|
-
|
|
873
|
-
|
|
1013
|
+
def allocate[A](resource: Resource[A]): $[A]
|
|
1014
|
+
def allocate[A <: AutoCloseable](value: => A): $[A]
|
|
874
1015
|
|
|
875
|
-
|
|
876
|
-
def $[A, B](scoped: A @@ this.Tag)(f: A => B): B @@ Tag
|
|
1016
|
+
infix transparent inline def $[A, B](sa: $[A])(inline f: A => B): B | $[B]
|
|
877
1017
|
|
|
878
|
-
|
|
879
|
-
def execute[A](scoped: A @@ this.Tag): A @@ Tag
|
|
1018
|
+
def lower[A](value: parent.$[A]): $[A]
|
|
880
1019
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1020
|
+
override def defer(f: => Unit): DeferHandle
|
|
1021
|
+
|
|
1022
|
+
def open(): $[Scope.OpenScope]
|
|
1023
|
+
|
|
1024
|
+
inline def leak[A](inline sa: $[A]): A
|
|
886
1025
|
```
|
|
887
1026
|
|
|
888
|
-
|
|
1027
|
+
Notes:
|
|
1028
|
+
|
|
1029
|
+
- `$` requires a **lambda literal** and enforces safe receiver-only usage.
|
|
1030
|
+
- `$` returns `B` if `Unscoped[B]` exists; otherwise returns `$[B]`.
|
|
1031
|
+
- If the scope is closed, `$` / `allocate` / `open` / `lower` return default values and perform no work.
|
|
1032
|
+
|
|
1033
|
+
Syntax enrichments available after `import scope.*` inside a scope:
|
|
1034
|
+
|
|
1035
|
+
```scala
|
|
1036
|
+
implicit class ScopedResourceOps[A](sr: $[Resource[A]]):
|
|
1037
|
+
def allocate: $[A]
|
|
1038
|
+
|
|
1039
|
+
implicit class ResourceOps[A](r: Resource[A]):
|
|
1040
|
+
def allocate: $[A]
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
### `Scope.global`
|
|
1046
|
+
|
|
1047
|
+
```scala
|
|
1048
|
+
object Scope:
|
|
1049
|
+
object global extends Scope
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
Properties:
|
|
1053
|
+
|
|
1054
|
+
- `type $[+A] = A` (identity)
|
|
1055
|
+
- `isOwner` always returns `true`
|
|
1056
|
+
- JVM: finalizers run at shutdown via a shutdown hook
|
|
1057
|
+
- Scala.js: shutdown hook is not available
|
|
1058
|
+
|
|
1059
|
+
---
|
|
1060
|
+
|
|
1061
|
+
### `Scope.OpenScope`
|
|
1062
|
+
|
|
1063
|
+
```scala
|
|
1064
|
+
case class OpenScope(scope: Scope, close: () => Finalization)
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
- `scope`: the child scope
|
|
1068
|
+
- `close()`: detaches from parent, runs child finalizers (LIFO), returns `Finalization`
|
|
1069
|
+
|
|
1070
|
+
---
|
|
1071
|
+
|
|
1072
|
+
### `Finalizer`
|
|
1073
|
+
|
|
1074
|
+
```scala
|
|
1075
|
+
trait Finalizer:
|
|
1076
|
+
def defer(f: => Unit): DeferHandle
|
|
1077
|
+
```
|
|
1078
|
+
|
|
1079
|
+
A minimal capability interface for registering cleanup.
|
|
1080
|
+
|
|
1081
|
+
Also available as a package-level helper:
|
|
1082
|
+
|
|
1083
|
+
```scala
|
|
1084
|
+
def defer(finalizer: => Unit)(using fin: Finalizer): DeferHandle
|
|
1085
|
+
```
|
|
1086
|
+
|
|
1087
|
+
---
|
|
1088
|
+
|
|
1089
|
+
### `DeferHandle`
|
|
1090
|
+
|
|
1091
|
+
```scala
|
|
1092
|
+
abstract class DeferHandle:
|
|
1093
|
+
def cancel(): Unit
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
- `cancel()` is thread-safe and idempotent
|
|
1097
|
+
- cancellation is O(1) (true removal from a concurrent map)
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
1101
|
+
### `Finalization`
|
|
1102
|
+
|
|
1103
|
+
```scala
|
|
1104
|
+
final class Finalization(val errors: zio.blocks.chunk.Chunk[Throwable]):
|
|
1105
|
+
def isEmpty: Boolean
|
|
1106
|
+
def nonEmpty: Boolean
|
|
1107
|
+
def orThrow(): Unit
|
|
1108
|
+
def suppress(initial: Throwable): Throwable
|
|
1109
|
+
|
|
1110
|
+
object Finalization:
|
|
1111
|
+
val empty: Finalization
|
|
1112
|
+
def apply(errors: Chunk[Throwable]): Finalization
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
### `Resource[+A]`
|
|
889
1118
|
|
|
890
1119
|
```scala
|
|
891
|
-
sealed trait Resource[+A]
|
|
1120
|
+
sealed trait Resource[+A]:
|
|
1121
|
+
def map[B](f: A => B): Resource[B]
|
|
1122
|
+
def flatMap[B](f: A => Resource[B]): Resource[B]
|
|
1123
|
+
def zip[B](that: Resource[B]): Resource[(A, B)]
|
|
1124
|
+
```
|
|
892
1125
|
|
|
893
|
-
|
|
1126
|
+
Companion constructors:
|
|
1127
|
+
|
|
1128
|
+
```scala
|
|
1129
|
+
object Resource:
|
|
894
1130
|
def apply[A](value: => A): Resource[A]
|
|
895
|
-
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
|
|
896
1131
|
def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
|
|
1132
|
+
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
|
|
1133
|
+
def shared[A](f: Scope => A): Resource[A]
|
|
1134
|
+
def unique[A](f: Scope => A): Resource[A]
|
|
897
1135
|
|
|
898
|
-
|
|
899
|
-
def from[T](wires: Wire[?, ?]*): Resource[T]
|
|
900
|
-
|
|
901
|
-
// Internal (used by generated code):
|
|
902
|
-
def shared[A](f: Finalizer => A): Resource[A]
|
|
903
|
-
def unique[A](f: Finalizer => A): Resource[A]
|
|
904
|
-
}
|
|
1136
|
+
inline def from[T]: Resource[T]
|
|
1137
|
+
inline def from[T](inline wires: Wire[?, ?]*): Resource[T]
|
|
905
1138
|
```
|
|
906
1139
|
|
|
907
|
-
|
|
1140
|
+
Notes:
|
|
1141
|
+
|
|
1142
|
+
- `Resource.from[T]` (no args) only works when `T` has **no non-scope dependencies** (constructor params may include `Scope`/`Finalizer`).
|
|
1143
|
+
- Use `Resource.from[T](wires*)` to provide/override dependencies and derive the full graph.
|
|
1144
|
+
|
|
1145
|
+
---
|
|
1146
|
+
|
|
1147
|
+
### `Wire[-In, +Out]`
|
|
908
1148
|
|
|
909
1149
|
```scala
|
|
910
|
-
sealed trait Wire[-In, +Out]
|
|
1150
|
+
sealed trait Wire[-In, +Out]:
|
|
911
1151
|
def isShared: Boolean
|
|
1152
|
+
def isUnique: Boolean = !isShared
|
|
1153
|
+
|
|
912
1154
|
def shared: Wire.Shared[In, Out]
|
|
913
1155
|
def unique: Wire.Unique[In, Out]
|
|
1156
|
+
|
|
914
1157
|
def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
|
|
915
|
-
|
|
1158
|
+
```
|
|
916
1159
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1160
|
+
Wires:
|
|
1161
|
+
|
|
1162
|
+
```scala
|
|
1163
|
+
object Wire:
|
|
1164
|
+
final case class Shared[-In, +Out](makeFn: (Scope, Context[In]) => Out) extends Wire[In, Out]
|
|
1165
|
+
final case class Unique[-In, +Out](makeFn: (Scope, Context[In]) => Out) extends Wire[In, Out]
|
|
1166
|
+
|
|
1167
|
+
def apply[T](t: T): Wire.Shared[Any, T]
|
|
1168
|
+
|
|
1169
|
+
transparent inline def shared[T]: Wire.Shared[?, T]
|
|
1170
|
+
transparent inline def unique[T]: Wire.Unique[?, T]
|
|
928
1171
|
```
|
|
929
1172
|
|
|
1173
|
+
Notes:
|
|
1174
|
+
|
|
1175
|
+
- `Wire(t)` wraps a pre-existing value; if it's `AutoCloseable`, `close()` is registered automatically when used.
|
|
1176
|
+
|
|
930
1177
|
---
|
|
931
1178
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
- `
|
|
947
|
-
-
|
|
1179
|
+
### `Unscoped[A]`
|
|
1180
|
+
|
|
1181
|
+
```scala
|
|
1182
|
+
trait Unscoped[A]
|
|
1183
|
+
|
|
1184
|
+
object Unscoped:
|
|
1185
|
+
inline given derived[A](using scala.deriving.Mirror.Of[A]): Unscoped[A]
|
|
1186
|
+
// plus many built-in givens (primitives, collections, time, UUID, Chunk, ...)
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
---
|
|
1190
|
+
|
|
1191
|
+
## Practical guidance (summary)
|
|
1192
|
+
|
|
1193
|
+
- Allocate in a scope: `resource.allocate` (inside `Scope.global.scoped { scope => import scope.* ... }`)
|
|
1194
|
+
- Use scoped values only through: `(scope $ value)(...)`
|
|
1195
|
+
- Return only `Unscoped` data from `scoped` blocks
|
|
1196
|
+
- Use `lower` to use parent values inside a child
|
|
1197
|
+
- If `$` returns `$[Resource[A]]`, call `.allocate` on it (scoped resource chaining)
|
|
1198
|
+
- Use `open()` for explicitly-managed, cross-thread capable scopes
|
|
1199
|
+
- Use `leak` only when interop forces it; prefer `Unscoped` for pure data
|