@zio.dev/zio-blocks 0.0.22 → 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 +42 -39
- package/package.json +1 -1
- package/reference/codec.md +20 -18
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +4 -0
- package/reference/json-schema.md +1 -1
- package/reference/json.md +2 -2
- package/reference/media-type.md +460 -0
- package/reference/optics.md +4 -0
- package/reference/schema-expr.md +669 -0
- package/reference/schema.md +1 -0
- package/reference/type-class-derivation.md +2 -1
- package/scope.md +744 -490
- package/sidebars.js +12 -0
- package/undocumented-report.md +331 -0
package/scope.md
CHANGED
|
@@ -1,386 +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
|
-
- **Zero-cost opaque type** (`$[A]` is the scoped type, equal to `A` at runtime)
|
|
11
|
-
- **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
|
|
12
|
-
- **Eager evaluation** (all operations execute immediately, no deferred thunks)
|
|
12
|
+
## Why Scope?
|
|
13
13
|
|
|
14
|
-
|
|
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:
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
- [Chaining resource acquisition](#chaining-resource-acquisition)
|
|
31
|
-
- [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
|
|
32
|
-
- [Classes with `Finalizer` parameters](#classes-with-finalizer-parameters)
|
|
33
|
-
- [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
|
|
34
|
-
- [Dependency injection with `Resource.from[T](wires*)`](#dependency-injection-with-resourcefromtwires)
|
|
35
|
-
- [Injecting traits via subtype wires](#injecting-traits-via-subtype-wires)
|
|
36
|
-
- [Interop escape hatch: `leak`](#interop-escape-hatch-leak)
|
|
37
|
-
- [Common compile errors](#common-compile-errors)
|
|
38
|
-
- [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**.
|
|
39
33
|
|
|
40
34
|
---
|
|
41
35
|
|
|
42
|
-
## Quick start
|
|
36
|
+
## Quick start (Scala 3)
|
|
43
37
|
|
|
44
38
|
```scala
|
|
45
|
-
import zio.blocks.scope
|
|
39
|
+
import zio.blocks.scope.*
|
|
46
40
|
|
|
47
|
-
final class Database extends AutoCloseable
|
|
41
|
+
final class Database extends AutoCloseable:
|
|
48
42
|
def query(sql: String): String = s"result: $sql"
|
|
49
43
|
def close(): Unit = println("db closed")
|
|
50
|
-
}
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
@main def quickStart(): Unit =
|
|
46
|
+
val out: String =
|
|
47
|
+
Scope.global.scoped { scope =>
|
|
48
|
+
import scope.*
|
|
54
49
|
|
|
55
|
-
|
|
50
|
+
val db: $[Database] =
|
|
51
|
+
Resource.fromAutoCloseable(new Database).allocate
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
53
|
+
// Safe access: the lambda parameter can only be used as a receiver
|
|
54
|
+
$(db)(_.query("SELECT 1"))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
println(out)
|
|
61
58
|
```
|
|
62
59
|
|
|
63
|
-
Key
|
|
60
|
+
Key points:
|
|
64
61
|
|
|
65
|
-
- `allocate(...)` returns a **scoped
|
|
66
|
-
-
|
|
67
|
-
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
- The `scoped` method requires `Unscoped[A]` evidence on the return type
|
|
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.
|
|
71
67
|
|
|
72
68
|
---
|
|
73
69
|
|
|
74
|
-
## Core
|
|
70
|
+
## Core mental model
|
|
75
71
|
|
|
76
|
-
### 1) `Scope
|
|
72
|
+
### 1) `Scope`: finalizers + type identity
|
|
77
73
|
|
|
78
|
-
`Scope` is a
|
|
74
|
+
`Scope` is a finalizer registry plus a unique type identity:
|
|
79
75
|
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
- **`val parent: Parent`** — reference to the parent scope.
|
|
76
|
+
- `type $[+A]` — a scope-tagged, path-dependent type (erases to `A` at runtime)
|
|
77
|
+
- `type Parent <: Scope` / `val parent: Parent` — the scope hierarchy
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```scala
|
|
87
|
-
type $[+A] // = A at runtime (zero-cost)
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
So in code you'll typically write:
|
|
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
|
-
import scope
|
|
95
|
-
val x: $[
|
|
83
|
+
import scope.*
|
|
84
|
+
val x: $[Int] = 1 // ok (in global, $[A] = A)
|
|
96
85
|
}
|
|
97
86
|
```
|
|
98
87
|
|
|
99
|
-
Child scopes are represented by `Scope.Child[P <: Scope]`, a `final class` nested in the `Scope` companion object.
|
|
100
|
-
|
|
101
88
|
#### Global scope
|
|
102
89
|
|
|
103
|
-
`Scope.global` is the root
|
|
90
|
+
`Scope.global` is the root:
|
|
104
91
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
type $[+A] = A
|
|
109
|
-
type Parent = global.type
|
|
110
|
-
val parent: Parent = this
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
- The global scope is intended to live for the lifetime of the process.
|
|
116
|
-
- Its finalizers run on JVM shutdown.
|
|
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
|
|
117
95
|
|
|
118
96
|
---
|
|
119
97
|
|
|
120
|
-
### 2) Scoped values: `$[
|
|
98
|
+
### 2) Scoped values: `scope.$[A]` / `$[A]`
|
|
99
|
+
|
|
100
|
+
A value of type `scope.$[A]` means:
|
|
121
101
|
|
|
122
|
-
|
|
102
|
+
> "This is an `A`, but it is only valid while `scope` is alive."
|
|
123
103
|
|
|
124
|
-
|
|
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
|
|
127
|
-
- **Access paths:**
|
|
128
|
-
- `scope.use(a)(f)` to apply a function and get `$[B]`
|
|
104
|
+
Properties:
|
|
129
105
|
|
|
130
|
-
|
|
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
|
|
131
109
|
|
|
132
|
-
|
|
110
|
+
#### Access operator: `(scope $ value)(f)`
|
|
111
|
+
|
|
112
|
+
The intended way to use a scoped value is:
|
|
133
113
|
|
|
134
114
|
```scala
|
|
135
|
-
|
|
136
|
-
|
|
115
|
+
(scope $ scopedValue)(a => a.method(...))
|
|
116
|
+
```
|
|
137
117
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
118
|
+
This is enforced by a macro that checks the lambda uses its parameter only in **receiver position**.
|
|
119
|
+
|
|
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
|
+
```
|
|
128
|
+
|
|
129
|
+
Rejected at compile time:
|
|
130
|
+
|
|
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
|
|
142
136
|
```
|
|
143
137
|
|
|
144
|
-
-
|
|
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)
|
|
138
|
+
##### "Auto-unwrap" rule (`Unscoped`)
|
|
147
139
|
|
|
148
|
-
|
|
140
|
+
`$` *auto-unwraps* when the result type is known to be safe data:
|
|
149
141
|
|
|
150
|
-
|
|
142
|
+
- if `B: Unscoped` → `(scope $ sa)(f)` returns **`B`**
|
|
143
|
+
- otherwise → it returns **`scope.$[B]`**
|
|
151
144
|
|
|
152
145
|
```scala
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
Scope.global.scoped { scope =>
|
|
147
|
+
import scope.*
|
|
155
148
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
149
|
+
val db: $[Database] = Resource.from[Database].allocate
|
|
150
|
+
|
|
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
|
+
}
|
|
159
154
|
```
|
|
160
155
|
|
|
161
156
|
---
|
|
162
157
|
|
|
163
158
|
### 3) `Resource[A]`: acquisition + finalization
|
|
164
159
|
|
|
165
|
-
`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).
|
|
166
161
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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).
|
|
170
177
|
|
|
171
|
-
|
|
178
|
+
#### Composition
|
|
172
179
|
|
|
173
|
-
|
|
174
|
-
- Wraps a by-name value; if it's `AutoCloseable`, `close()` is registered automatically.
|
|
175
|
-
- `Resource.acquireRelease(acquire)(release)`
|
|
176
|
-
- Explicit lifecycle.
|
|
177
|
-
- `Resource.fromAutoCloseable(thunk)`
|
|
178
|
-
- A type-safe helper for `AutoCloseable`.
|
|
179
|
-
- `Resource.from[T](wires*)` (macro)
|
|
180
|
-
- The primary entry point for dependency injection.
|
|
181
|
-
- Resolves `T` and all its dependencies into a single `Resource[T]`.
|
|
182
|
-
- Auto-creates missing wires using `Wire.shared` for concrete classes.
|
|
183
|
-
- Requires explicit wires for: primitives, functions, collections, and abstract types.
|
|
184
|
-
- If `T` or any dependency is `AutoCloseable`, registers `close()` automatically.
|
|
180
|
+
`Resource` composes with:
|
|
185
181
|
|
|
186
|
-
|
|
182
|
+
- `map`
|
|
183
|
+
- `flatMap`
|
|
184
|
+
- `zip`
|
|
187
185
|
|
|
188
|
-
|
|
186
|
+
Finalizers remain tied to the allocation scope; in composed resources, finalizers still run LIFO.
|
|
189
187
|
|
|
190
|
-
|
|
191
|
-
- Produces a **fresh** instance every time you allocate it (typical for `Resource(...)`, `acquireRelease`, etc.).
|
|
192
|
-
- `Resource.Shared[A]`
|
|
193
|
-
- Produces a **shared** instance per `Resource.Shared` value, with **reference counting**:
|
|
194
|
-
- the first allocation initializes the value and collects finalizers
|
|
195
|
-
- each allocating scope registers a decrement finalizer
|
|
196
|
-
- when the reference count reaches zero, the collected finalizers run
|
|
188
|
+
#### Sharing vs uniqueness (important)
|
|
197
189
|
|
|
198
|
-
|
|
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.
|
|
199
200
|
|
|
200
201
|
---
|
|
201
202
|
|
|
202
|
-
### 4) `Unscoped`:
|
|
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).
|
|
203
212
|
|
|
204
|
-
|
|
213
|
+
#### Deriving / defining your own instances
|
|
205
214
|
|
|
206
|
-
|
|
207
|
-
- Primitives: `Int`, `Long`, `Boolean`, `Double`, etc.
|
|
208
|
-
- `String`, `Unit`, `Nothing`
|
|
209
|
-
- Collections of Unscoped types
|
|
215
|
+
Scala 3 (derivation via `Unscoped.derived`):
|
|
210
216
|
|
|
211
|
-
**Custom Unscoped types:**
|
|
212
217
|
```scala
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
218
|
+
import zio.blocks.scope.*
|
|
219
|
+
|
|
220
|
+
final case class Config(debug: Boolean)
|
|
221
|
+
object Config:
|
|
216
222
|
given Unscoped[Config] = Unscoped.derived
|
|
217
|
-
|
|
223
|
+
```
|
|
218
224
|
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
Scala 2.13:
|
|
226
|
+
|
|
227
|
+
```scala
|
|
228
|
+
import zio.blocks.scope.*
|
|
229
|
+
|
|
230
|
+
final case class Config(debug: Boolean)
|
|
221
231
|
object Config {
|
|
222
232
|
implicit val unscopedConfig: Unscoped[Config] = Unscoped.derived[Config]
|
|
223
233
|
}
|
|
224
234
|
```
|
|
225
235
|
|
|
226
|
-
|
|
236
|
+
#### Scope boundary example
|
|
227
237
|
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
```scala
|
|
239
|
+
import zio.blocks.scope.*
|
|
230
240
|
|
|
231
|
-
|
|
241
|
+
Scope.global.scoped { parent =>
|
|
242
|
+
import parent.*
|
|
243
|
+
|
|
244
|
+
val ok: String =
|
|
245
|
+
parent.scoped { child =>
|
|
246
|
+
"hello" // String is Unscoped
|
|
247
|
+
}
|
|
232
248
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### 5) `lower`: using a parent-scoped value in a child scope
|
|
263
|
+
|
|
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:
|
|
236
265
|
|
|
237
266
|
```scala
|
|
238
|
-
|
|
239
|
-
import parent._
|
|
267
|
+
import zio.blocks.scope.*
|
|
240
268
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"hello"
|
|
244
|
-
}
|
|
269
|
+
Scope.global.scoped { outer =>
|
|
270
|
+
import outer.*
|
|
245
271
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
}
|
|
251
279
|
}
|
|
252
280
|
```
|
|
253
281
|
|
|
282
|
+
This is safe because **parents always outlive children** (child finalizers run before the parent closes).
|
|
283
|
+
|
|
254
284
|
---
|
|
255
285
|
|
|
256
|
-
###
|
|
286
|
+
### 6) `defer`: manual finalizers (+ cancellation)
|
|
257
287
|
|
|
258
|
-
|
|
288
|
+
Use `defer` to register cleanup. It returns a `DeferHandle` you can cancel.
|
|
259
289
|
|
|
260
290
|
```scala
|
|
261
|
-
|
|
262
|
-
import parent._
|
|
291
|
+
import zio.blocks.scope.*
|
|
263
292
|
|
|
264
|
-
|
|
293
|
+
Scope.global.scoped { scope =>
|
|
294
|
+
import scope.*
|
|
265
295
|
|
|
266
|
-
|
|
267
|
-
import child._
|
|
296
|
+
val in = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
|
|
268
297
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
child.use(db)(_.query("SELECT 1"))
|
|
298
|
+
val h: DeferHandle =
|
|
299
|
+
defer(in.close())
|
|
272
300
|
|
|
273
|
-
|
|
274
|
-
|
|
301
|
+
val first = in.read()
|
|
302
|
+
println(first)
|
|
303
|
+
|
|
304
|
+
// If you already cleaned up manually:
|
|
305
|
+
// h.cancel() // thread-safe, idempotent
|
|
275
306
|
}
|
|
276
307
|
```
|
|
277
308
|
|
|
278
|
-
|
|
309
|
+
There is also a **package-level** helper that only requires a `Finalizer`:
|
|
310
|
+
|
|
311
|
+
```scala
|
|
312
|
+
import zio.blocks.scope.*
|
|
313
|
+
|
|
314
|
+
Scope.global.scoped { scope =>
|
|
315
|
+
import scope.*
|
|
316
|
+
given Finalizer = scope
|
|
317
|
+
|
|
318
|
+
defer(println("cleanup")) // uses the package-level helper
|
|
319
|
+
}
|
|
320
|
+
```
|
|
279
321
|
|
|
280
322
|
---
|
|
281
323
|
|
|
282
|
-
###
|
|
324
|
+
### 7) `open()`: non-lexical, explicitly-managed child scopes
|
|
283
325
|
|
|
284
|
-
`
|
|
326
|
+
`scoped` ties lifetime to a block. `open()` creates a child scope you close explicitly.
|
|
285
327
|
|
|
286
|
-
-
|
|
287
|
-
-
|
|
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
|
|
288
331
|
|
|
289
|
-
|
|
332
|
+
From `Scope.global` the returned type is `Scope.OpenScope` directly (because global `$[A] = A`):
|
|
290
333
|
|
|
291
|
-
|
|
292
|
-
|
|
334
|
+
```scala
|
|
335
|
+
import zio.blocks.scope.*
|
|
293
336
|
|
|
294
|
-
|
|
337
|
+
val os: Scope.OpenScope = Scope.global.open()
|
|
295
338
|
|
|
296
|
-
|
|
339
|
+
val db = os.scope.allocate(Resource.fromAutoCloseable(new Database))
|
|
297
340
|
|
|
298
|
-
|
|
341
|
+
// ... use db ...
|
|
299
342
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
| `Wire.shared[T]` | Create a shared wire from `T`'s constructor |
|
|
303
|
-
| `Wire.unique[T]` | Create a unique wire from `T`'s constructor |
|
|
304
|
-
| `Resource.from[T](wires*)` | Wire up `T` and all dependencies into a `Resource` |
|
|
343
|
+
os.close().orThrow()
|
|
344
|
+
```
|
|
305
345
|
|
|
306
|
-
|
|
346
|
+
Inside a child scope, `open()` returns `$[Scope.OpenScope]`. Prefer using it safely via `$`:
|
|
307
347
|
|
|
308
|
-
|
|
348
|
+
```scala
|
|
349
|
+
import zio.blocks.scope.*
|
|
309
350
|
|
|
310
|
-
|
|
351
|
+
Scope.global.scoped { parent =>
|
|
352
|
+
import parent.*
|
|
311
353
|
|
|
312
|
-
|
|
313
|
-
2. **Validate**: Checks for cycles, unmakeable types, duplicate providers
|
|
314
|
-
3. **Topological sort**: Orders dependencies so leaves are allocated first
|
|
315
|
-
4. **Generate composition**: Produces a `Resource[T]` via flatMap chains
|
|
354
|
+
val os: $[Scope.OpenScope] = open()
|
|
316
355
|
|
|
317
|
-
|
|
356
|
+
$(os) { h =>
|
|
357
|
+
val child = h.scope
|
|
358
|
+
val db = child.allocate(Resource.fromAutoCloseable(new Database))
|
|
318
359
|
|
|
319
|
-
|
|
320
|
-
|
|
360
|
+
// ...
|
|
361
|
+
h.close().orThrow()
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
321
365
|
|
|
322
|
-
|
|
366
|
+
---
|
|
323
367
|
|
|
324
|
-
|
|
368
|
+
### 8) Escape hatch: `leak`
|
|
325
369
|
|
|
326
|
-
|
|
370
|
+
Sometimes you must hand a raw value to code that cannot work with `$[A]`. Use `leak`:
|
|
371
|
+
|
|
372
|
+
```scala
|
|
373
|
+
import zio.blocks.scope.*
|
|
374
|
+
|
|
375
|
+
Scope.global.scoped { scope =>
|
|
376
|
+
import scope.*
|
|
377
|
+
|
|
378
|
+
val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
379
|
+
|
|
380
|
+
val raw: Database = leak(db) // emits a compiler warning
|
|
381
|
+
// thirdParty(raw)
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
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.
|
|
327
386
|
|
|
328
387
|
---
|
|
329
388
|
|
|
330
389
|
## Safety model (why leaking is prevented)
|
|
331
390
|
|
|
332
|
-
|
|
391
|
+
Scope's safety comes from *three reinforcing layers*.
|
|
392
|
+
|
|
393
|
+
### 1) Type barrier: scope-specific `$[A]`
|
|
333
394
|
|
|
334
|
-
|
|
395
|
+
Every scope has a distinct `$[A]` type. You cannot accidentally use values across scopes without an explicit conversion (`lower` for parent → child).
|
|
335
396
|
|
|
336
|
-
###
|
|
397
|
+
### 2) Controlled access: `$` macro restricts lambda usage
|
|
337
398
|
|
|
338
|
-
|
|
399
|
+
The `$` operator only allows using the unwrapped value as a **method/field receiver**. This prevents:
|
|
400
|
+
|
|
401
|
+
- returning the resource
|
|
402
|
+
- storing it in a local val/var
|
|
403
|
+
- passing it as an argument
|
|
404
|
+
- capturing it in a closure
|
|
405
|
+
|
|
406
|
+
Also note: `$` requires a **lambda literal**. Method references / variables are rejected:
|
|
339
407
|
|
|
340
408
|
```scala
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
import child._
|
|
345
|
-
// allocate in child
|
|
346
|
-
}
|
|
347
|
-
}
|
|
409
|
+
// does not compile:
|
|
410
|
+
val f: Database => String = _.query("x")
|
|
411
|
+
(scope $ db)(f) // "$ requires a lambda literal ..."
|
|
348
412
|
```
|
|
349
413
|
|
|
350
|
-
|
|
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`).
|
|
419
|
+
|
|
420
|
+
### Closed-scope defense (runtime)
|
|
421
|
+
|
|
422
|
+
If a scope escapes and is used after closing, operations become **no-ops returning default values**:
|
|
351
423
|
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
354
428
|
|
|
355
|
-
|
|
429
|
+
This prevents post-close interaction with released resources, but can produce surprising default values if scopes are misused across threads.
|
|
356
430
|
|
|
357
|
-
|
|
431
|
+
### Thread ownership rule (JVM)
|
|
358
432
|
|
|
359
|
-
|
|
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.)
|
|
360
438
|
|
|
361
439
|
---
|
|
362
440
|
|
|
363
|
-
## Usage examples
|
|
441
|
+
## Usage examples (patterns)
|
|
364
442
|
|
|
365
443
|
### Allocating and using a resource
|
|
366
444
|
|
|
367
445
|
```scala
|
|
368
|
-
import zio.blocks.scope
|
|
446
|
+
import zio.blocks.scope.*
|
|
369
447
|
|
|
370
|
-
final class FileHandle(path: String) extends AutoCloseable
|
|
448
|
+
final class FileHandle(path: String) extends AutoCloseable:
|
|
371
449
|
def readAll(): String = s"contents of $path"
|
|
372
450
|
def close(): Unit = println(s"closed $path")
|
|
373
|
-
}
|
|
374
451
|
|
|
375
|
-
|
|
376
|
-
|
|
452
|
+
@main def fileExample(): Unit =
|
|
453
|
+
Scope.global.scoped { scope =>
|
|
454
|
+
import scope.*
|
|
377
455
|
|
|
378
|
-
|
|
456
|
+
val h: $[FileHandle] =
|
|
457
|
+
Resource(new FileHandle("data.txt")).allocate
|
|
379
458
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
459
|
+
val contents: String =
|
|
460
|
+
$(h)(_.readAll())
|
|
461
|
+
|
|
462
|
+
println(contents)
|
|
463
|
+
}
|
|
384
464
|
```
|
|
385
465
|
|
|
386
466
|
---
|
|
@@ -388,312 +468,385 @@ Scope.global.scoped { scope =>
|
|
|
388
468
|
### Nested scopes (child can use parent, not vice versa)
|
|
389
469
|
|
|
390
470
|
```scala
|
|
391
|
-
import zio.blocks.scope
|
|
471
|
+
import zio.blocks.scope.*
|
|
392
472
|
|
|
393
|
-
|
|
394
|
-
|
|
473
|
+
final class Database extends AutoCloseable:
|
|
474
|
+
def query(sql: String): String = s"result: $sql"
|
|
475
|
+
def close(): Unit = println("db closed")
|
|
395
476
|
|
|
396
|
-
|
|
477
|
+
@main def nested(): Unit =
|
|
478
|
+
Scope.global.scoped { parent =>
|
|
479
|
+
import parent.*
|
|
397
480
|
|
|
398
|
-
|
|
399
|
-
import child._
|
|
481
|
+
val parentDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
400
482
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
483
|
+
val done: String =
|
|
484
|
+
parent.scoped { child =>
|
|
485
|
+
import child.*
|
|
404
486
|
|
|
405
|
-
|
|
487
|
+
val db: $[Database] = lower(parentDb)
|
|
488
|
+
println($(db)(_.query("SELECT 1")))
|
|
406
489
|
|
|
407
|
-
|
|
408
|
-
|
|
490
|
+
val childDb: $[Database] = Resource.fromAutoCloseable(new Database).allocate
|
|
491
|
+
println($(childDb)(_.query("SELECT 2")))
|
|
409
492
|
|
|
410
|
-
|
|
411
|
-
|
|
493
|
+
// childDb cannot be returned to the parent (not Unscoped)
|
|
494
|
+
"done"
|
|
495
|
+
}
|
|
412
496
|
|
|
413
|
-
|
|
414
|
-
|
|
497
|
+
println($(parentDb)(_.query("SELECT 3")))
|
|
498
|
+
done
|
|
415
499
|
}
|
|
416
|
-
|
|
417
|
-
// parentDb is still usable here:
|
|
418
|
-
println(parent.use(parentDb)(_.query("SELECT 3")))
|
|
419
|
-
}
|
|
420
500
|
```
|
|
421
501
|
|
|
502
|
+
Finalizers run **child first, then parent**.
|
|
503
|
+
|
|
422
504
|
---
|
|
423
505
|
|
|
424
|
-
### Chaining resource acquisition
|
|
506
|
+
### Chaining resource acquisition (`$[Resource[A]]` + `.allocate`)
|
|
425
507
|
|
|
426
|
-
|
|
508
|
+
If a method returns `Resource[A]`, `$` returns a **scoped** `Resource[A]` (because `Resource[A]` is not `Unscoped`). Allocate it without leaking:
|
|
427
509
|
|
|
428
510
|
```scala
|
|
429
|
-
import zio.blocks.scope
|
|
511
|
+
import zio.blocks.scope.*
|
|
430
512
|
|
|
431
|
-
class Pool extends AutoCloseable
|
|
432
|
-
def lease():
|
|
513
|
+
final class Pool extends AutoCloseable:
|
|
514
|
+
def lease(): Resource[Conn] = Resource.fromAutoCloseable(new Conn)
|
|
433
515
|
def close(): Unit = println("pool closed")
|
|
434
|
-
}
|
|
435
516
|
|
|
436
|
-
class
|
|
517
|
+
final class Conn extends AutoCloseable:
|
|
437
518
|
def query(sql: String): String = s"result: $sql"
|
|
438
519
|
def close(): Unit = println("connection closed")
|
|
439
|
-
}
|
|
440
520
|
|
|
441
|
-
|
|
442
|
-
|
|
521
|
+
@main def chaining(): Unit =
|
|
522
|
+
Scope.global.scoped { scope =>
|
|
523
|
+
import scope.*
|
|
443
524
|
|
|
444
|
-
|
|
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")
|
|
525
|
+
val pool: $[Pool] = Resource.fromAutoCloseable(new Pool).allocate
|
|
450
526
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
+
}
|
|
455
536
|
```
|
|
456
537
|
|
|
538
|
+
This `.allocate` comes from `Scope.ScopedResourceOps` (an extension on `$[Resource[A]]`).
|
|
539
|
+
|
|
457
540
|
---
|
|
458
541
|
|
|
459
|
-
###
|
|
542
|
+
### Allocating a bare `Resource[A]` with `.allocate`
|
|
460
543
|
|
|
461
|
-
|
|
544
|
+
A plain `Resource[A]` also has `.allocate` as syntax sugar for `scope.allocate(resource)`:
|
|
462
545
|
|
|
463
546
|
```scala
|
|
464
|
-
import zio.blocks.scope
|
|
547
|
+
import zio.blocks.scope.*
|
|
465
548
|
|
|
466
549
|
Scope.global.scoped { scope =>
|
|
467
|
-
import scope
|
|
550
|
+
import scope.*
|
|
468
551
|
|
|
469
|
-
val
|
|
552
|
+
val db: $[Database] =
|
|
553
|
+
Resource.fromAutoCloseable(new Database).allocate
|
|
470
554
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
val firstByte = handle.read()
|
|
474
|
-
println(firstByte)
|
|
555
|
+
$(db)(_.query("SELECT 1"))
|
|
475
556
|
}
|
|
476
557
|
```
|
|
477
558
|
|
|
478
|
-
|
|
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.
|
|
479
564
|
|
|
480
565
|
```scala
|
|
481
|
-
import zio.blocks.scope
|
|
566
|
+
import zio.blocks.scope.*
|
|
482
567
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
given
|
|
568
|
+
final case class Config(url: String)
|
|
569
|
+
object Config:
|
|
570
|
+
given Unscoped[Config] = Unscoped.derived
|
|
486
571
|
|
|
487
|
-
|
|
488
|
-
}
|
|
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
|
+
}
|
|
489
587
|
```
|
|
490
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
|
+
|
|
491
594
|
---
|
|
492
595
|
|
|
493
|
-
### Classes with `
|
|
596
|
+
### Classes with `Scope` parameters (scope injection)
|
|
494
597
|
|
|
495
|
-
If
|
|
598
|
+
If a class needs to allocate resources or create child scopes, accept a `Scope`:
|
|
496
599
|
|
|
497
600
|
```scala
|
|
498
|
-
import zio.blocks.scope
|
|
601
|
+
import zio.blocks.scope.*
|
|
499
602
|
|
|
500
|
-
class
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
def getConnection(): Connection = pool.acquire()
|
|
505
|
-
}
|
|
603
|
+
final case class Config(url: String)
|
|
604
|
+
object Config:
|
|
605
|
+
given Unscoped[Config] = Unscoped.derived
|
|
506
606
|
|
|
507
|
-
|
|
508
|
-
|
|
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")
|
|
509
610
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
}
|
|
515
631
|
```
|
|
516
632
|
|
|
517
|
-
|
|
518
|
-
- `Finalizer` is the minimal interface—it only has `defer`
|
|
519
|
-
- Classes that need cleanup should not have access to `allocate` or `use`
|
|
520
|
-
- 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.
|
|
521
634
|
|
|
522
635
|
---
|
|
523
636
|
|
|
524
|
-
|
|
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`.
|
|
525
640
|
|
|
526
|
-
|
|
641
|
+
### `Wire[-In, +Out]`: a dependency recipe
|
|
642
|
+
|
|
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
|
|
527
649
|
|
|
528
650
|
```scala
|
|
529
|
-
import zio.blocks.scope
|
|
651
|
+
import zio.blocks.scope.*
|
|
530
652
|
import zio.blocks.context.Context
|
|
531
653
|
|
|
532
654
|
final case class Config(debug: Boolean)
|
|
655
|
+
object Config:
|
|
656
|
+
given Unscoped[Config] = Unscoped.derived
|
|
533
657
|
|
|
534
|
-
val w: Wire.Shared[Boolean, Config] =
|
|
535
|
-
|
|
658
|
+
val w: Wire.Shared[Boolean, Config] =
|
|
659
|
+
Wire.shared[Config] // Boolean => Config
|
|
536
660
|
|
|
537
|
-
|
|
538
|
-
|
|
661
|
+
val deps: Context[Boolean] =
|
|
662
|
+
Context(true)
|
|
539
663
|
|
|
540
|
-
|
|
664
|
+
@main def wireAndContext(): Unit =
|
|
665
|
+
Scope.global.scoped { scope =>
|
|
666
|
+
import scope.*
|
|
541
667
|
|
|
542
|
-
|
|
668
|
+
val cfg: $[Config] =
|
|
669
|
+
allocate(w.toResource(deps))
|
|
543
670
|
|
|
544
|
-
|
|
545
|
-
|
|
671
|
+
val debug: Boolean =
|
|
672
|
+
$(cfg)(_.debug)
|
|
673
|
+
|
|
674
|
+
println(debug)
|
|
675
|
+
}
|
|
546
676
|
```
|
|
547
677
|
|
|
548
|
-
Sharing vs uniqueness at the wire level
|
|
678
|
+
#### Sharing vs uniqueness at the wire level
|
|
549
679
|
|
|
550
680
|
```scala
|
|
551
|
-
import zio.blocks.scope
|
|
681
|
+
import zio.blocks.scope.*
|
|
552
682
|
|
|
553
|
-
val ws = Wire.shared[Config] // shared recipe
|
|
554
|
-
val wu = Wire.unique[Config] // unique recipe
|
|
683
|
+
val ws = Wire.shared[Config] // shared recipe
|
|
684
|
+
val wu = Wire.unique[Config] // unique recipe
|
|
555
685
|
```
|
|
556
686
|
|
|
687
|
+
The difference is realized when converting to resources (`toResource`) and allocating.
|
|
688
|
+
|
|
557
689
|
---
|
|
558
690
|
|
|
559
|
-
###
|
|
691
|
+
### `Resource.from[T](wires*)`: derive a whole object graph
|
|
692
|
+
|
|
693
|
+
`Resource.from[T](wires*)` is the primary entry point for DI. It:
|
|
560
694
|
|
|
561
|
-
|
|
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:
|
|
562
702
|
|
|
563
703
|
```scala
|
|
564
|
-
import zio.blocks.scope
|
|
704
|
+
import zio.blocks.scope.*
|
|
565
705
|
|
|
566
706
|
final case class Config(url: String)
|
|
707
|
+
object Config:
|
|
708
|
+
given Unscoped[Config] = Unscoped.derived
|
|
567
709
|
|
|
568
|
-
final class Logger
|
|
710
|
+
final class Logger:
|
|
569
711
|
def info(msg: String): Unit = println(msg)
|
|
570
|
-
}
|
|
571
712
|
|
|
572
|
-
final class Database(cfg: Config) extends AutoCloseable
|
|
713
|
+
final class Database(cfg: Config) extends AutoCloseable:
|
|
573
714
|
def query(sql: String): String = s"[${cfg.url}] $sql"
|
|
574
715
|
def close(): Unit = println("database closed")
|
|
575
|
-
}
|
|
576
716
|
|
|
577
|
-
final class Service(db: Database, logger: Logger) extends AutoCloseable
|
|
717
|
+
final class Service(db: Database, logger: Logger) extends AutoCloseable:
|
|
578
718
|
def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
|
|
579
719
|
def close(): Unit = println("service closed")
|
|
580
|
-
}
|
|
581
720
|
|
|
582
|
-
// Only provide leaf values (primitives, configs) - the rest is auto-wired:
|
|
583
721
|
val serviceResource: Resource[Service] =
|
|
584
722
|
Resource.from[Service](
|
|
585
|
-
Wire(Config("jdbc:postgresql://localhost/db"))
|
|
723
|
+
Wire(Config("jdbc:postgresql://localhost/db")) // leaf value
|
|
586
724
|
)
|
|
587
725
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
// Then: service closed, database closed (LIFO order)
|
|
726
|
+
@main def di(): Unit =
|
|
727
|
+
Scope.global.scoped { scope =>
|
|
728
|
+
import scope.*
|
|
729
|
+
val svc: $[Service] = serviceResource.allocate
|
|
730
|
+
$(svc)(_.run())
|
|
731
|
+
}
|
|
595
732
|
```
|
|
596
733
|
|
|
597
|
-
|
|
598
|
-
- Leaf values: primitives, configs, pre-existing instances via `Wire(value)`
|
|
599
|
-
- Abstract types: traits/abstract classes via `Wire.shared[ConcreteImpl]`
|
|
600
|
-
- Overrides: when you want `unique` instead of the default `shared`
|
|
734
|
+
#### Injecting traits via subtype wires
|
|
601
735
|
|
|
602
|
-
|
|
603
|
-
- Concrete classes with accessible primary constructors (default: `Wire.shared`)
|
|
604
|
-
|
|
605
|
-
---
|
|
606
|
-
|
|
607
|
-
### Injecting traits via subtype wires
|
|
608
|
-
|
|
609
|
-
When a dependency is a trait or abstract class, provide a wire for a concrete implementation:
|
|
736
|
+
When a dependency is abstract, provide a wire for a concrete implementation:
|
|
610
737
|
|
|
611
738
|
```scala
|
|
612
|
-
import zio.blocks.scope
|
|
739
|
+
import zio.blocks.scope.*
|
|
613
740
|
|
|
614
|
-
trait Logger
|
|
741
|
+
trait Logger:
|
|
615
742
|
def info(msg: String): Unit
|
|
616
|
-
}
|
|
617
743
|
|
|
618
|
-
final class ConsoleLogger extends Logger
|
|
744
|
+
final class ConsoleLogger extends Logger:
|
|
619
745
|
def info(msg: String): Unit = println(msg)
|
|
620
|
-
}
|
|
621
746
|
|
|
622
|
-
final class App(logger: Logger)
|
|
747
|
+
final class App(logger: Logger):
|
|
623
748
|
def run(): Unit = logger.info("Hello!")
|
|
624
|
-
}
|
|
625
749
|
|
|
626
|
-
// Wire.shared[ConsoleLogger] satisfies the Logger dependency via subtyping:
|
|
627
750
|
val appResource: Resource[App] =
|
|
628
751
|
Resource.from[App](
|
|
629
|
-
Wire.shared[ConsoleLogger]
|
|
752
|
+
Wire.shared[ConsoleLogger] // satisfies Logger via subtyping
|
|
630
753
|
)
|
|
631
754
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
755
|
+
@main def traitInjection(): Unit =
|
|
756
|
+
Scope.global.scoped { scope =>
|
|
757
|
+
import scope.*
|
|
758
|
+
val app: $[App] = appResource.allocate
|
|
759
|
+
$(app)(_.run())
|
|
760
|
+
}
|
|
637
761
|
```
|
|
638
762
|
|
|
639
|
-
|
|
763
|
+
#### Diamond patterns share a single instance (when appropriate)
|
|
640
764
|
|
|
641
765
|
```scala
|
|
766
|
+
import zio.blocks.scope.*
|
|
767
|
+
|
|
642
768
|
trait Service
|
|
643
|
-
class LiveService extends Service
|
|
644
|
-
class NeedsService(s: Service)
|
|
645
|
-
class NeedsLive(l: LiveService)
|
|
646
|
-
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)
|
|
647
773
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
)
|
|
652
|
-
//
|
|
774
|
+
val appResource: Resource[App] =
|
|
775
|
+
Resource.from[App](
|
|
776
|
+
Wire.shared[LiveService]
|
|
777
|
+
)
|
|
778
|
+
// LiveService instantiations: 1
|
|
653
779
|
```
|
|
654
780
|
|
|
655
781
|
---
|
|
656
782
|
|
|
657
|
-
|
|
783
|
+
## Common compile errors (and what they mean)
|
|
658
784
|
|
|
659
|
-
|
|
785
|
+
This module produces two kinds of compile-time feedback:
|
|
660
786
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
Scope.global.scoped { scope =>
|
|
665
|
-
import scope._
|
|
666
|
-
val db: $[Database] = allocate(Resource(new Database))
|
|
787
|
+
- **Plain macro aborts** for unsafe `$` usage
|
|
788
|
+
- **ASCII-rendered** errors/warnings for DI derivation + leak warnings (via `internal.ErrorMessages`)
|
|
667
789
|
|
|
668
|
-
|
|
669
|
-
// thirdParty(raw)
|
|
670
|
-
}
|
|
671
|
-
```
|
|
790
|
+
### Unsafe use inside `$`
|
|
672
791
|
|
|
673
|
-
|
|
792
|
+
Typical messages include:
|
|
674
793
|
|
|
675
|
-
|
|
794
|
+
```
|
|
795
|
+
Unsafe use of scoped value: the lambda parameter cannot be passed as an argument to a function or method.
|
|
796
|
+
```
|
|
676
797
|
|
|
677
|
-
|
|
798
|
+
Other variants:
|
|
678
799
|
|
|
679
|
-
|
|
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.`
|
|
680
803
|
|
|
681
|
-
### Not a class (Wire.shared/unique on trait
|
|
804
|
+
### Not a class (`Wire.shared/unique` on a trait / abstract)
|
|
682
805
|
|
|
683
806
|
```
|
|
684
|
-
── Scope Error
|
|
807
|
+
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
685
808
|
|
|
686
809
|
Cannot derive Wire for MyTrait: not a class.
|
|
687
810
|
|
|
688
811
|
Hint: Use Wire.Shared / Wire.Unique directly.
|
|
689
812
|
|
|
690
|
-
|
|
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
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
691
844
|
```
|
|
692
845
|
|
|
693
846
|
### Unmakeable type (primitives, functions, collections)
|
|
694
847
|
|
|
695
848
|
```
|
|
696
|
-
── Scope Error
|
|
849
|
+
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
697
850
|
|
|
698
851
|
Cannot auto-create String
|
|
699
852
|
|
|
@@ -710,13 +863,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
710
863
|
...
|
|
711
864
|
)
|
|
712
865
|
|
|
713
|
-
|
|
866
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
714
867
|
```
|
|
715
868
|
|
|
716
|
-
### Abstract type (trait
|
|
869
|
+
### Abstract type (trait / abstract class dependency)
|
|
717
870
|
|
|
718
871
|
```
|
|
719
|
-
── Scope Error
|
|
872
|
+
── Scope Error ─────────────────────────────────────────────────────────────────
|
|
720
873
|
|
|
721
874
|
Cannot auto-create Logger
|
|
722
875
|
|
|
@@ -732,13 +885,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
732
885
|
...
|
|
733
886
|
)
|
|
734
887
|
|
|
735
|
-
|
|
888
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
736
889
|
```
|
|
737
890
|
|
|
738
891
|
### Duplicate providers (ambiguous wires)
|
|
739
892
|
|
|
740
893
|
```
|
|
741
|
-
── Scope Error
|
|
894
|
+
── Scope Error ────────────────────────────────────────────────────────────────
|
|
742
895
|
|
|
743
896
|
Multiple providers for Service
|
|
744
897
|
|
|
@@ -748,13 +901,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
748
901
|
|
|
749
902
|
Hint: Remove duplicate wires or use distinct wrapper types.
|
|
750
903
|
|
|
751
|
-
|
|
904
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
752
905
|
```
|
|
753
906
|
|
|
754
907
|
### Dependency cycle
|
|
755
908
|
|
|
756
909
|
```
|
|
757
|
-
── Scope Error
|
|
910
|
+
── Scope Error ────────────────────────────────────────────────────────────────
|
|
758
911
|
|
|
759
912
|
Dependency cycle detected
|
|
760
913
|
|
|
@@ -770,13 +923,13 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
770
923
|
• Using lazy initialization
|
|
771
924
|
• Restructuring dependencies
|
|
772
925
|
|
|
773
|
-
|
|
926
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
774
927
|
```
|
|
775
928
|
|
|
776
929
|
### Subtype conflict (related dependency types)
|
|
777
930
|
|
|
778
931
|
```
|
|
779
|
-
── Scope Error
|
|
932
|
+
── Scope Error ────────────────────────────────────────────────────────────────
|
|
780
933
|
|
|
781
934
|
Dependency type conflict in MyService
|
|
782
935
|
|
|
@@ -789,16 +942,16 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
789
942
|
To fix this, wrap one or both types in a distinct wrapper:
|
|
790
943
|
|
|
791
944
|
case class WrappedInputStream(value: InputStream)
|
|
792
|
-
|
|
945
|
+
or
|
|
793
946
|
opaque type WrappedInputStream = InputStream
|
|
794
947
|
|
|
795
|
-
|
|
948
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
796
949
|
```
|
|
797
950
|
|
|
798
|
-
### Duplicate parameter types in constructor
|
|
951
|
+
### Duplicate parameter types in a constructor
|
|
799
952
|
|
|
800
953
|
```
|
|
801
|
-
── Scope Error
|
|
954
|
+
── Scope Error ────────────────────────────────────────────────────────────────
|
|
802
955
|
|
|
803
956
|
Constructor of App has multiple parameters of type String
|
|
804
957
|
|
|
@@ -807,139 +960,240 @@ The scope macros produce beautiful, actionable compile-time error messages with
|
|
|
807
960
|
Fix: Wrap one parameter in an opaque type to distinguish them:
|
|
808
961
|
|
|
809
962
|
opaque type FirstString = String
|
|
810
|
-
|
|
963
|
+
or
|
|
811
964
|
case class FirstString(value: String)
|
|
812
965
|
|
|
813
|
-
|
|
966
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
814
967
|
```
|
|
815
968
|
|
|
816
969
|
### Leak warning
|
|
817
970
|
|
|
818
|
-
When using `leak(value)` to escape the scoped type system:
|
|
819
|
-
|
|
820
971
|
```
|
|
821
|
-
── Scope Warning
|
|
972
|
+
── Scope Warning ───────────────────────────────────────────────────────────────
|
|
822
973
|
|
|
823
974
|
leak(db)
|
|
824
975
|
^
|
|
825
976
|
|
|
|
826
977
|
|
|
827
|
-
Warning: db is being leaked from scope
|
|
978
|
+
Warning: db is being leaked from scope zio.blocks.scope.Scope.Child[...].
|
|
828
979
|
This may result in undefined behavior.
|
|
829
980
|
|
|
830
981
|
Hint:
|
|
831
982
|
If you know this data type is not resourceful, then add an Unscoped
|
|
832
983
|
instance for it so you do not need to leak it.
|
|
833
984
|
|
|
834
|
-
|
|
985
|
+
───────────────────────────────────────────────────────────────────────────────
|
|
835
986
|
```
|
|
836
987
|
|
|
837
988
|
---
|
|
838
989
|
|
|
839
|
-
## 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).
|
|
840
993
|
|
|
841
994
|
### `Scope`
|
|
842
995
|
|
|
843
|
-
|
|
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:
|
|
844
1009
|
|
|
845
1010
|
```scala
|
|
846
|
-
|
|
847
|
-
type $[+A] // = A at runtime (zero-cost)
|
|
848
|
-
type Parent <: Scope
|
|
849
|
-
val parent: Parent
|
|
1011
|
+
def scoped[A](f: (child: Scope.Child[this.type]) => A)(using Unscoped[A]): A
|
|
850
1012
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
def defer(f: => Unit): Unit
|
|
1013
|
+
def allocate[A](resource: Resource[A]): $[A]
|
|
1014
|
+
def allocate[A <: AutoCloseable](value: => A): $[A]
|
|
854
1015
|
|
|
855
|
-
|
|
856
|
-
def use[A, B](scoped: $[A])(f: A => B): $[B]
|
|
1016
|
+
infix transparent inline def $[A, B](sa: $[A])(inline f: A => B): B | $[B]
|
|
857
1017
|
|
|
858
|
-
|
|
859
|
-
def $[A](a: A): $[A]
|
|
1018
|
+
def lower[A](value: parent.$[A]): $[A]
|
|
860
1019
|
|
|
861
|
-
|
|
862
|
-
def lower[A](value: parent.$[A]): $[A]
|
|
1020
|
+
override def defer(f: => Unit): DeferHandle
|
|
863
1021
|
|
|
864
|
-
|
|
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
|
|
1022
|
+
def open(): $[Scope.OpenScope]
|
|
869
1023
|
|
|
870
|
-
|
|
871
|
-
|
|
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
|
|
1024
|
+
inline def leak[A](inline sa: $[A]): A
|
|
1025
|
+
```
|
|
875
1026
|
|
|
876
|
-
|
|
877
|
-
def map[B](f: A => B): $[B]
|
|
878
|
-
def flatMap[B](f: A => $[B]): $[B]
|
|
879
|
-
}
|
|
1027
|
+
Notes:
|
|
880
1028
|
|
|
881
|
-
|
|
882
|
-
|
|
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]
|
|
883
1041
|
```
|
|
884
1042
|
|
|
885
|
-
|
|
1043
|
+
---
|
|
1044
|
+
|
|
1045
|
+
### `Scope.global`
|
|
886
1046
|
|
|
887
1047
|
```scala
|
|
888
|
-
|
|
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`
|
|
889
1062
|
|
|
890
|
-
|
|
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]`
|
|
1118
|
+
|
|
1119
|
+
```scala
|
|
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
|
+
```
|
|
1125
|
+
|
|
1126
|
+
Companion constructors:
|
|
1127
|
+
|
|
1128
|
+
```scala
|
|
1129
|
+
object Resource:
|
|
891
1130
|
def apply[A](value: => A): Resource[A]
|
|
892
|
-
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
|
|
893
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]
|
|
894
1135
|
|
|
895
|
-
|
|
896
|
-
def from[T]: Resource[T]
|
|
897
|
-
def from[T](wires: Wire[?, ?]*): Resource[T] // with dependency wires
|
|
898
|
-
|
|
899
|
-
// Internal (used by generated code):
|
|
900
|
-
def shared[A](f: Finalizer => A): Resource[A]
|
|
901
|
-
def unique[A](f: Finalizer => A): Resource[A]
|
|
902
|
-
}
|
|
1136
|
+
inline def from[T]: Resource[T]
|
|
1137
|
+
inline def from[T](inline wires: Wire[?, ?]*): Resource[T]
|
|
903
1138
|
```
|
|
904
1139
|
|
|
905
|
-
|
|
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]`
|
|
906
1148
|
|
|
907
1149
|
```scala
|
|
908
|
-
sealed trait Wire[-In, +Out]
|
|
1150
|
+
sealed trait Wire[-In, +Out]:
|
|
909
1151
|
def isShared: Boolean
|
|
1152
|
+
def isUnique: Boolean = !isShared
|
|
1153
|
+
|
|
910
1154
|
def shared: Wire.Shared[In, Out]
|
|
911
1155
|
def unique: Wire.Unique[In, Out]
|
|
1156
|
+
|
|
912
1157
|
def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
|
|
913
|
-
|
|
1158
|
+
```
|
|
914
1159
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
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]
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
Notes:
|
|
1174
|
+
|
|
1175
|
+
- `Wire(t)` wraps a pre-existing value; if it's `AutoCloseable`, `close()` is registered automatically when used.
|
|
1176
|
+
|
|
1177
|
+
---
|
|
1178
|
+
|
|
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, ...)
|
|
926
1187
|
```
|
|
927
1188
|
|
|
928
1189
|
---
|
|
929
1190
|
|
|
930
|
-
##
|
|
931
|
-
|
|
932
|
-
-
|
|
933
|
-
-
|
|
934
|
-
-
|
|
935
|
-
- Use `
|
|
936
|
-
- `$[A]
|
|
937
|
-
-
|
|
938
|
-
- Use `
|
|
939
|
-
- Return `Unscoped` types from child scopes to extract raw values.
|
|
940
|
-
- If it doesn't typecheck, it would have been unsafe at runtime.
|
|
941
|
-
|
|
942
|
-
**The 3 macro entry points:**
|
|
943
|
-
- `Wire.shared[T]` — shared wire from constructor
|
|
944
|
-
- `Wire.unique[T]` — unique wire from constructor
|
|
945
|
-
- `Resource.from[T](wires*)` — wire up T and all dependencies
|
|
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
|