@zio.dev/zio-blocks 0.0.1 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.md +143 -15
- package/package.json +5 -2
- package/reference/binding.md +8 -14
- package/reference/chunk.md +36 -36
- package/reference/docs.md +2 -2
- package/reference/dynamic-value.md +34 -34
- package/reference/formats.md +16 -16
- package/reference/json-schema.md +20 -20
- package/reference/json.md +47 -47
- package/reference/optics.md +51 -37
- package/reference/reflect.md +3 -3
- package/reference/registers.md +7 -7
- package/reference/schema.md +18 -18
- package/reference/syntax.md +16 -16
- package/scope.md +481 -161
- package/reference/modifier.md +0 -276
- package/reference/reflect-transform.md +0 -387
- package/reference/type-class-derivation-internals.md +0 -632
package/scope.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
`zio.blocks.scope` provides **compile-time verified resource safety** for synchronous code by tagging values with an unnameable, type-level **scope identity**. Values allocated in a scope can only be used when you hold a compatible `Scope`, and values allocated in a *child* scope cannot be returned to the parent in a usable form.
|
|
4
4
|
|
|
5
|
+
**Structured scopes.** Scopes follow the structured-concurrency philosophy: child scopes are nested within parent scopes, resources are tied to the lifetime of the scope that allocated them, and cleanup happens deterministically when the scope exits (finalizers run LIFO). This "nesting = lifetime" structure provides clear ownership boundaries in addition to compile-time leak prevention.
|
|
6
|
+
|
|
5
7
|
If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the same problem space, but it focuses on:
|
|
6
8
|
|
|
7
9
|
- **Compile-time prevention of scope leaks**
|
|
8
|
-
- **
|
|
10
|
+
- **Unified scoped type** (`A @@ S` is a type alias for `Scoped[A, S]`)
|
|
9
11
|
- **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
|
|
10
12
|
|
|
11
13
|
---
|
|
@@ -17,10 +19,10 @@ If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the
|
|
|
17
19
|
- [1) `Scope[ParentTag, Tag]`](#1-scopeparenttag-tag)
|
|
18
20
|
- [2) Scoped values: `A @@ S`](#2-scoped-values-a--s)
|
|
19
21
|
- [3) `Resource[A]`: acquisition + finalization](#3-resourcea-acquisition--finalization)
|
|
20
|
-
- [4) `
|
|
21
|
-
- [5) `
|
|
22
|
-
- [6) `
|
|
23
|
-
- [7) `
|
|
22
|
+
- [4) `A @@ S`: unified scoped type](#4-a--s-unified-scoped-type)
|
|
23
|
+
- [5) `Unscoped`: marking pure data types](#5-unscoped-marking-pure-data-types)
|
|
24
|
+
- [6) `ScopeLift[A, S]`: what may return from child scopes](#6-scopelifta-s-what-may-return-from-child-scopes)
|
|
25
|
+
- [7) `Wire[-In, +Out]`: dependency recipes](#7-wire-in-out-dependency-recipes)
|
|
24
26
|
- [Safety model (why leaking is prevented)](#safety-model-why-leaking-is-prevented)
|
|
25
27
|
- [Usage examples](#usage-examples)
|
|
26
28
|
- [Allocating and using a resource](#allocating-and-using-a-resource)
|
|
@@ -28,9 +30,10 @@ If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the
|
|
|
28
30
|
- [Building a `Scoped` program (map/flatMap)](#building-a-scoped-program-mapflatmap)
|
|
29
31
|
- [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
|
|
30
32
|
- [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
+
- [Dependency injection with `Resource.from[T](wires*)`](#dependency-injection-with-resourcefromtwires)
|
|
34
|
+
- [Injecting traits via subtype wires](#injecting-traits-via-subtype-wires)
|
|
33
35
|
- [Interop escape hatch: `leak`](#interop-escape-hatch-leak)
|
|
36
|
+
- [Common compile errors](#common-compile-errors)
|
|
34
37
|
- [API reference (selected)](#api-reference-selected)
|
|
35
38
|
|
|
36
39
|
---
|
|
@@ -49,10 +52,12 @@ Scope.global.scoped { scope =>
|
|
|
49
52
|
val db: Database @@ scope.Tag =
|
|
50
53
|
scope.allocate(Resource(new Database))
|
|
51
54
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
// $ executes immediately and returns String @@ scope.Tag
|
|
56
|
+
// Use the function to work with the value
|
|
57
|
+
(scope $ db) { database =>
|
|
58
|
+
val result = database.query("SELECT 1")
|
|
59
|
+
println(result)
|
|
60
|
+
}
|
|
56
61
|
}
|
|
57
62
|
```
|
|
58
63
|
|
|
@@ -60,7 +65,8 @@ Key things to notice:
|
|
|
60
65
|
|
|
61
66
|
- `scope.allocate(...)` returns a **scoped** value: `Database @@ scope.Tag`
|
|
62
67
|
- You **cannot** call `db.query(...)` directly (methods are intentionally hidden)
|
|
63
|
-
- You must use `scope
|
|
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
|
|
64
70
|
- When the `scoped { ... }` block exits, finalizers run **LIFO** and errors are handled safely
|
|
65
71
|
|
|
66
72
|
---
|
|
@@ -102,24 +108,33 @@ object Scope {
|
|
|
102
108
|
|
|
103
109
|
- The global scope is intended to live for the lifetime of the process.
|
|
104
110
|
- Its finalizers run on JVM shutdown.
|
|
105
|
-
- Values allocated in `Scope.global`
|
|
111
|
+
- Values allocated in `Scope.global` can escape as raw values via `ScopeLift.globalScope` (see below).
|
|
106
112
|
|
|
107
113
|
---
|
|
108
114
|
|
|
109
115
|
### 2) Scoped values: `A @@ S`
|
|
110
116
|
|
|
111
|
-
`A @@ S`
|
|
117
|
+
`A @@ S` is a type alias for `Scoped[A, S]` — a handle to a value of type `A` that is locked to scope tag `S`.
|
|
112
118
|
|
|
113
|
-
- **Runtime representation:**
|
|
114
|
-
- **Key effect:** methods on `A` are hidden
|
|
119
|
+
- **Runtime representation:** a boxed thunk (lightweight wrapper)
|
|
120
|
+
- **Key effect:** methods on `A` are hidden; you can't call `a.method` directly
|
|
121
|
+
- **Acquisition timing:** `scope.allocate(resource)` acquires the resource **immediately** (eagerly) and returns a scoped handle for accessing the already-acquired value. The thunk defers *access*, not *acquisition*.
|
|
115
122
|
- **Access paths:**
|
|
116
|
-
- `scope
|
|
117
|
-
- `a.map / a.flatMap` to build
|
|
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
|
|
118
126
|
|
|
119
|
-
#### Scala
|
|
127
|
+
#### Scala 2 note
|
|
120
128
|
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
In Scala 2, explicit type annotations are required when assigning scoped values to avoid existential type inference issues:
|
|
130
|
+
|
|
131
|
+
```scala
|
|
132
|
+
// Scala 2 requires explicit type annotation
|
|
133
|
+
val db: Database @@ scope.Tag = scope.allocate(Resource[Database])
|
|
134
|
+
|
|
135
|
+
// Scala 3 can infer the type
|
|
136
|
+
val db = scope.allocate(Resource[Database])
|
|
137
|
+
```
|
|
123
138
|
|
|
124
139
|
---
|
|
125
140
|
|
|
@@ -139,12 +154,12 @@ Common constructors:
|
|
|
139
154
|
- Explicit lifecycle.
|
|
140
155
|
- `Resource.fromAutoCloseable(thunk)`
|
|
141
156
|
- A type-safe helper for `AutoCloseable`.
|
|
142
|
-
- `Resource.from[T]` (macro)
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
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.
|
|
148
163
|
|
|
149
164
|
#### Resource "sharing" vs "uniqueness"
|
|
150
165
|
|
|
@@ -162,93 +177,172 @@ Common constructors:
|
|
|
162
177
|
|
|
163
178
|
---
|
|
164
179
|
|
|
165
|
-
### 4) `
|
|
180
|
+
### 4) `A @@ S`: unified scoped type
|
|
166
181
|
|
|
167
|
-
`Scoped[
|
|
182
|
+
`A @@ S` (type alias for `Scoped[A, S]`) is the core type representing a deferred computation that produces `A` and requires scope tag `S` to execute.
|
|
168
183
|
|
|
169
184
|
Execution happens via:
|
|
170
185
|
|
|
171
186
|
```scala
|
|
172
|
-
scope(scopedComputation)
|
|
187
|
+
scope.execute(scopedComputation)
|
|
173
188
|
```
|
|
174
189
|
|
|
175
190
|
How to build them:
|
|
176
191
|
|
|
177
|
-
- From
|
|
178
|
-
- `
|
|
179
|
-
|
|
180
|
-
-
|
|
181
|
-
- `
|
|
192
|
+
- From `scope.allocate`:
|
|
193
|
+
- `scope.allocate(resource)` returns `A @@ scope.Tag`
|
|
194
|
+
- Using combinators:
|
|
195
|
+
- `(a: A @@ S).map(f: A => B)` returns `B @@ S`
|
|
196
|
+
- `(a: A @@ S).flatMap(f: A => B @@ T)` returns `B @@ (S & T)` (Scala 3) / `B @@ (S with T)` (Scala 2)
|
|
197
|
+
- Use for-comprehensions to chain scoped computations
|
|
198
|
+
- From ordinary values:
|
|
199
|
+
- `Scoped(value)` lifts a value into an `A @@ Any` (which can be used anywhere due to contravariance)
|
|
200
|
+
|
|
201
|
+
**Contravariance:** `A @@ S` is contravariant in `S`. This means `A @@ ParentTag` is a subtype of `A @@ ChildTag` when `ChildTag <: ParentTag`. Child scopes can execute parent-scoped computations automatically.
|
|
202
|
+
|
|
203
|
+
**For-comprehension example:**
|
|
204
|
+
|
|
205
|
+
```scala
|
|
206
|
+
Scope.global.scoped { scope =>
|
|
207
|
+
val program: Result @@ scope.Tag = for {
|
|
208
|
+
pool <- scope.allocate(Resource[Pool])
|
|
209
|
+
conn <- scope.allocate(Resource(pool.lease()))
|
|
210
|
+
data <- conn.map(_.query("SELECT *"))
|
|
211
|
+
} yield process(data)
|
|
212
|
+
|
|
213
|
+
scope.execute(program)
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### 5) `Unscoped`: marking pure data types
|
|
220
|
+
|
|
221
|
+
The `Unscoped[A]` typeclass marks types as pure data that don't hold resources. This is used by `ScopeLift` to determine what can escape from child scopes.
|
|
222
|
+
|
|
223
|
+
**Built-in Unscoped types:**
|
|
224
|
+
- Primitives: `Int`, `Long`, `Boolean`, `Double`, etc.
|
|
225
|
+
- `String`, `Unit`, `Nothing`
|
|
226
|
+
- Collections of Unscoped types
|
|
227
|
+
|
|
228
|
+
**Custom Unscoped types:**
|
|
229
|
+
```scala
|
|
230
|
+
case class Config(debug: Boolean)
|
|
231
|
+
object Config {
|
|
232
|
+
given Unscoped[Config] = Unscoped.derived
|
|
233
|
+
}
|
|
234
|
+
```
|
|
182
235
|
|
|
183
|
-
|
|
236
|
+
**Important:** `$` and `execute` always return `B @@ scope.Tag`. They never escape values directly. Escape only happens at the `.scoped` boundary via `ScopeLift`.
|
|
184
237
|
|
|
185
238
|
---
|
|
186
239
|
|
|
187
|
-
###
|
|
240
|
+
### 6) `ScopeLift[A, S]`: what may return from child scopes
|
|
241
|
+
|
|
242
|
+
When you call `scope.scoped { child => ... }`, the return type `A` must have a `ScopeLift[A, S]` instance where `S` is the parent scope's tag. This typeclass both **gates** what can exit child scopes and **transforms** the return type.
|
|
243
|
+
|
|
244
|
+
**ScopeLift instances and their behavior:**
|
|
245
|
+
|
|
246
|
+
| Instance | Matches | Output Type |
|
|
247
|
+
|----------|---------|-------------|
|
|
248
|
+
| `globalScope[A]` | Any `A` when `S = GlobalTag` | `A` (unchanged) |
|
|
249
|
+
| `nothing[S]` | `Nothing` | `Nothing` |
|
|
250
|
+
| `unscoped[A, S]` | `A` with `Unscoped[A]` | `A` (raw value) |
|
|
251
|
+
| `scoped[B, T, S]` | `B @@ T` when `S <:< T` | `B @@ T` (unchanged) |
|
|
188
252
|
|
|
189
|
-
|
|
253
|
+
**Allowed return types:**
|
|
190
254
|
|
|
191
|
-
- `
|
|
192
|
-
- `
|
|
255
|
+
- **`Unscoped` types**: Pure data lifts to raw `A`
|
|
256
|
+
- **Parent-scoped values**: `B @@ T` where `S <:< T` lifts as-is
|
|
257
|
+
- **`Nothing`**: For blocks that throw
|
|
258
|
+
- **Anything from global scope**: Global scope never closes
|
|
193
259
|
|
|
194
|
-
|
|
260
|
+
**Rejected return types (no ScopeLift instance):**
|
|
195
261
|
|
|
196
|
-
-
|
|
197
|
-
-
|
|
262
|
+
- **Closures**: `() => A` could capture the child scope
|
|
263
|
+
- **Child-scoped values**: `B @@ child.Tag` would be use-after-close
|
|
264
|
+
- **The scope itself**: Would allow operations after close
|
|
198
265
|
|
|
199
|
-
|
|
266
|
+
```scala
|
|
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
|
+
}
|
|
272
|
+
|
|
273
|
+
// ✅ OK: parent-tagged value outlives child - lifts as-is
|
|
274
|
+
val parentDb: Database @@ parent.Tag = parent.allocate(Resource[Database])
|
|
275
|
+
val same: Database @@ parent.Tag = parent.scoped { _ =>
|
|
276
|
+
parentDb
|
|
277
|
+
}
|
|
200
278
|
|
|
201
|
-
|
|
202
|
-
|
|
279
|
+
// ❌ COMPILE ERROR: closure has no ScopeLift instance
|
|
280
|
+
// val leak = parent.scoped { child =>
|
|
281
|
+
// val db = child.allocate(Resource[Database])
|
|
282
|
+
// () => println("captured!") // Function types rejected
|
|
283
|
+
// }
|
|
284
|
+
|
|
285
|
+
// ❌ COMPILE ERROR: child-scoped value has no ScopeLift instance
|
|
286
|
+
// val escaped = parent.scoped { child =>
|
|
287
|
+
// child.allocate(Resource[Database]) // Database @@ child.Tag can't escape
|
|
288
|
+
// }
|
|
289
|
+
}
|
|
290
|
+
```
|
|
203
291
|
|
|
204
292
|
---
|
|
205
293
|
|
|
206
|
-
###
|
|
294
|
+
### 7) `Wire[-In, +Out]`: dependency recipes
|
|
207
295
|
|
|
208
|
-
`Wire` is a recipe for constructing services,
|
|
296
|
+
`Wire` is a recipe for constructing services. It describes **how** to build a service given its dependencies, but does not resolve those dependencies itself.
|
|
209
297
|
|
|
210
298
|
- `In` is the required dependencies (provided as a `Context[In]`)
|
|
211
299
|
- `Out` is the produced service
|
|
212
300
|
|
|
213
301
|
There are two wire flavors:
|
|
214
302
|
|
|
215
|
-
- `Wire.Shared`: a shared
|
|
216
|
-
- `Wire.Unique`: a
|
|
303
|
+
- `Wire.Shared`: produces a shared (memoized) instance
|
|
304
|
+
- `Wire.Unique`: produces a fresh instance each time
|
|
217
305
|
|
|
218
|
-
**Important clarification:** `Wire` itself is just a recipe. The
|
|
306
|
+
**Important clarification:** `Wire` itself is just a recipe. The sharing/uniqueness behavior is realized when the wire is used inside `Resource.from`, which composes `Resource.Shared` or `Resource.Unique` instances accordingly.
|
|
219
307
|
|
|
220
|
-
|
|
221
|
-
val r: Resource[Out] = wire.toResource(deps)
|
|
222
|
-
val out: Out @@ scope.Tag = scope.allocate(r)
|
|
223
|
-
```
|
|
308
|
+
#### Creating wires
|
|
224
309
|
|
|
225
|
-
|
|
226
|
-
- `Wire.Unique#toResource` produces a `Resource.Unique`.
|
|
310
|
+
There are exactly **3 macro entry points**:
|
|
227
311
|
|
|
228
|
-
|
|
312
|
+
| Macro | Purpose |
|
|
313
|
+
|-------|---------|
|
|
314
|
+
| `Wire.shared[T]` | Create a shared wire from `T`'s constructor |
|
|
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` |
|
|
229
317
|
|
|
230
|
-
|
|
231
|
-
- `unique[T]`: derive a unique wire
|
|
318
|
+
For wrapping pre-existing values:
|
|
232
319
|
|
|
233
|
-
|
|
320
|
+
- `Wire(value)` — wraps a value; if `AutoCloseable`, registers `close()` automatically
|
|
321
|
+
|
|
322
|
+
#### How `Resource.from[T](wires*)` works
|
|
234
323
|
|
|
235
|
-
|
|
324
|
+
1. **Collect wires**: Uses explicit wires when provided, otherwise auto-creates with `Wire.shared`
|
|
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
|
|
236
328
|
|
|
237
|
-
|
|
329
|
+
Key insight: **Compose Resources, don't accumulate values.** Each wire becomes a `Resource`, and they are composed via `flatMap`. This correctly preserves:
|
|
238
330
|
|
|
239
|
-
|
|
331
|
+
- **Sharing**: Same `Resource.Shared` instance → same value (even in diamond patterns)
|
|
332
|
+
- **Uniqueness**: `Resource.Unique` → fresh value per injection site
|
|
240
333
|
|
|
241
|
-
|
|
334
|
+
#### Subtype resolution
|
|
242
335
|
|
|
243
|
-
|
|
244
|
-
- `shared[MyTrait]` or `unique[MyTrait]` will pick it up automatically.
|
|
336
|
+
When `Resource.from` needs a dependency of type `Service`, it will accept a wire whose output is a subtype (e.g., `Wire.shared[LiveService]` where `LiveService extends Service`). This enables trait injection without extra boilerplate.
|
|
245
337
|
|
|
246
|
-
|
|
338
|
+
If the same concrete wire satisfies multiple types (e.g., `Service` and `LiveService`), only **one instance** is created and reused for both.
|
|
247
339
|
|
|
248
340
|
---
|
|
249
341
|
|
|
250
342
|
## Safety model (why leaking is prevented)
|
|
251
343
|
|
|
344
|
+
**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`). The guarantees are "good enough" to catch mistakes in regular usage, not protection against intentional circumvention.
|
|
345
|
+
|
|
252
346
|
The library prevents scope leaks via two reinforcing mechanisms:
|
|
253
347
|
|
|
254
348
|
### A) Existential child tags (fresh, unnameable types)
|
|
@@ -268,9 +362,11 @@ The child scope has an existential tag (fresh per invocation). You can allocate
|
|
|
268
362
|
Compile-time safety is verified in tests, e.g.:
|
|
269
363
|
`ScopeCompileTimeSafetyScala3Spec`.
|
|
270
364
|
|
|
271
|
-
### B)
|
|
365
|
+
### B) Contravariance prevents child-to-parent widening
|
|
272
366
|
|
|
273
|
-
|
|
367
|
+
`A @@ S` is contravariant in `S`. This means `Db @@ child.Tag` is **not** a subtype of `Db @@ parent.Tag` — the subtyping goes the *other* direction. A child scope can use parent-tagged values (because `child.Tag <: parent.Tag` makes `A @@ parent.Tag <: A @@ child.Tag`), but you cannot widen a child-tagged value to a parent tag.
|
|
368
|
+
|
|
369
|
+
Additionally, the thunk-based representation hides `A`'s methods — you can't call `db.query(...)` directly on a `Database @@ scope.Tag`. The only sanctioned access routes are `scope.$` and `scope.execute`, which require a scope with a compatible tag.
|
|
274
370
|
|
|
275
371
|
---
|
|
276
372
|
|
|
@@ -289,10 +385,11 @@ final class FileHandle(path: String) extends AutoCloseable {
|
|
|
289
385
|
Scope.global.scoped { scope =>
|
|
290
386
|
val h = scope.allocate(Resource(new FileHandle("data.txt")))
|
|
291
387
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
388
|
+
// $ executes immediately - work with the value inside the function
|
|
389
|
+
(scope $ h) { handle =>
|
|
390
|
+
val contents = handle.readAll()
|
|
391
|
+
println(contents)
|
|
392
|
+
}
|
|
296
393
|
}
|
|
297
394
|
```
|
|
298
395
|
|
|
@@ -308,23 +405,28 @@ Scope.global.scoped { parent =>
|
|
|
308
405
|
|
|
309
406
|
parent.scoped { child =>
|
|
310
407
|
// child can use parent-scoped values:
|
|
311
|
-
|
|
312
|
-
|
|
408
|
+
child.$(parentDb) { db =>
|
|
409
|
+
println(db.query("SELECT 1"))
|
|
410
|
+
}
|
|
313
411
|
|
|
314
412
|
val childDb = child.allocate(Resource(new Database))
|
|
315
413
|
|
|
316
414
|
// You can use childDb *inside* the child:
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
//
|
|
322
|
-
|
|
415
|
+
child.$(childDb) { db =>
|
|
416
|
+
println(db.query("SELECT 2"))
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Return an Unscoped value - ScopeLift extracts it
|
|
420
|
+
"done"
|
|
421
|
+
|
|
422
|
+
// But you cannot return childDb to the parent:
|
|
423
|
+
// childDb : Database @@ child.Tag has no ScopeLift instance
|
|
323
424
|
}
|
|
324
425
|
|
|
325
426
|
// parentDb is still usable here:
|
|
326
|
-
|
|
327
|
-
|
|
427
|
+
parent.$(parentDb) { db =>
|
|
428
|
+
println(db.query("SELECT 3"))
|
|
429
|
+
}
|
|
328
430
|
}
|
|
329
431
|
```
|
|
330
432
|
|
|
@@ -332,21 +434,54 @@ Scope.global.scoped { parent =>
|
|
|
332
434
|
|
|
333
435
|
### Building a `Scoped` program (map/flatMap)
|
|
334
436
|
|
|
437
|
+
You can use for-comprehensions to compose scoped computations. Each `<-` uses `flatMap`, accumulating requirements:
|
|
438
|
+
|
|
335
439
|
```scala
|
|
336
440
|
import zio.blocks.scope._
|
|
337
441
|
|
|
338
442
|
Scope.global.scoped { scope =>
|
|
339
|
-
val db = scope.allocate(Resource(new Database))
|
|
443
|
+
val db: Database @@ scope.Tag = scope.allocate(Resource(new Database))
|
|
444
|
+
|
|
445
|
+
// Build a scoped computation with map
|
|
446
|
+
val program: String @@ scope.Tag = db.map { d =>
|
|
447
|
+
d.query("SELECT 1")
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// execute runs the computation immediately, returns String @@ scope.Tag
|
|
451
|
+
// Use side effects inside the computation to observe results
|
|
452
|
+
scope.execute(program)
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
This pattern shines when chaining resource acquisition:
|
|
457
|
+
|
|
458
|
+
```scala
|
|
459
|
+
import zio.blocks.scope._
|
|
460
|
+
|
|
461
|
+
// A pool that leases connections
|
|
462
|
+
class Pool(implicit finalizer: Finalizer) extends AutoCloseable {
|
|
463
|
+
def lease: Resource[Connection] = Resource(new Connection)
|
|
464
|
+
def close(): Unit = println("pool closed")
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
class Connection extends AutoCloseable {
|
|
468
|
+
def query(sql: String): String = s"result: $sql"
|
|
469
|
+
def close(): Unit = println("connection closed")
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
Scope.global.scoped { scope =>
|
|
473
|
+
val pool: Pool @@ scope.Tag = scope.allocate(Resource.from[Pool])
|
|
340
474
|
|
|
341
|
-
val program:
|
|
475
|
+
val program: String @@ scope.Tag =
|
|
342
476
|
for {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
} yield
|
|
477
|
+
p <- pool // extract Pool from scoped
|
|
478
|
+
connection <- scope.allocate(p.lease) // allocate returns Connection @@ Tag
|
|
479
|
+
} yield connection.query("SELECT 1")
|
|
346
480
|
|
|
347
|
-
|
|
348
|
-
|
|
481
|
+
// execute runs the computation - connection is used inside the yield
|
|
482
|
+
scope.execute(program)
|
|
349
483
|
}
|
|
484
|
+
// Output: connection closed, then pool closed (LIFO)
|
|
350
485
|
```
|
|
351
486
|
|
|
352
487
|
---
|
|
@@ -375,6 +510,7 @@ import zio.blocks.scope._
|
|
|
375
510
|
|
|
376
511
|
Scope.global.scoped { scope =>
|
|
377
512
|
given Finalizer = scope
|
|
513
|
+
|
|
378
514
|
defer { println("cleanup") }
|
|
379
515
|
}
|
|
380
516
|
```
|
|
@@ -390,7 +526,7 @@ import zio.blocks.scope._
|
|
|
390
526
|
|
|
391
527
|
class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
|
|
392
528
|
private val pool = createPool(config)
|
|
393
|
-
|
|
529
|
+
defer { pool.shutdown() }
|
|
394
530
|
|
|
395
531
|
def getConnection(): Connection = pool.acquire()
|
|
396
532
|
}
|
|
@@ -413,13 +549,15 @@ Why `Finalizer` instead of `Scope`?
|
|
|
413
549
|
|
|
414
550
|
### Dependency injection with `Wire` + `Context`
|
|
415
551
|
|
|
552
|
+
For manual wiring (when you already have dependencies assembled), use `wire.toResource(ctx)`:
|
|
553
|
+
|
|
416
554
|
```scala
|
|
417
555
|
import zio.blocks.scope._
|
|
418
556
|
import zio.blocks.context.Context
|
|
419
557
|
|
|
420
558
|
final case class Config(debug: Boolean)
|
|
421
559
|
|
|
422
|
-
val w: Wire.Shared[Boolean, Config] = shared[Config]
|
|
560
|
+
val w: Wire.Shared[Boolean, Config] = Wire.shared[Config]
|
|
423
561
|
val deps: Context[Boolean] = Context[Boolean](true)
|
|
424
562
|
|
|
425
563
|
Scope.global.scoped { scope =>
|
|
@@ -427,7 +565,7 @@ Scope.global.scoped { scope =>
|
|
|
427
565
|
scope.allocate(w.toResource(deps))
|
|
428
566
|
|
|
429
567
|
val debug: Boolean =
|
|
430
|
-
scope
|
|
568
|
+
(scope $ cfg)(_.debug) // Boolean typically escapes
|
|
431
569
|
|
|
432
570
|
println(debug)
|
|
433
571
|
}
|
|
@@ -438,95 +576,105 @@ Sharing vs uniqueness at the wire level:
|
|
|
438
576
|
```scala
|
|
439
577
|
import zio.blocks.scope._
|
|
440
578
|
|
|
441
|
-
val ws = shared[Config] // shared recipe; sharing happens via Resource.Shared when allocated
|
|
442
|
-
val wu = unique[Config] // unique recipe; each allocation is fresh
|
|
579
|
+
val ws = Wire.shared[Config] // shared recipe; sharing happens via Resource.Shared when allocated
|
|
580
|
+
val wu = Wire.unique[Config] // unique recipe; each allocation is fresh
|
|
443
581
|
```
|
|
444
582
|
|
|
445
583
|
---
|
|
446
584
|
|
|
447
|
-
###
|
|
585
|
+
### Dependency injection with `Resource.from[T](wires*)`
|
|
448
586
|
|
|
449
|
-
`Resource.from[T]`
|
|
587
|
+
`Resource.from[T](wires*)` is the **primary entry point** for dependency injection. It resolves `T` and all its dependencies into a single `Resource[T]`.
|
|
450
588
|
|
|
451
589
|
```scala
|
|
452
590
|
import zio.blocks.scope._
|
|
453
|
-
import zio.blocks.context.Context
|
|
454
591
|
|
|
455
592
|
final case class Config(url: String)
|
|
456
593
|
|
|
457
|
-
|
|
458
|
-
def info(msg: String): Unit
|
|
594
|
+
final class Logger {
|
|
595
|
+
def info(msg: String): Unit = println(msg)
|
|
459
596
|
}
|
|
460
597
|
|
|
461
|
-
final class
|
|
462
|
-
def
|
|
598
|
+
final class Database(cfg: Config) extends AutoCloseable {
|
|
599
|
+
def query(sql: String): String = s"[${cfg.url}] $sql"
|
|
600
|
+
def close(): Unit = println("database closed")
|
|
463
601
|
}
|
|
464
602
|
|
|
465
|
-
final class Service(
|
|
466
|
-
def run(): Unit = logger.info(s"running with ${
|
|
603
|
+
final class Service(db: Database, logger: Logger) extends AutoCloseable {
|
|
604
|
+
def run(): Unit = logger.info(s"running with ${db.query("SELECT 1")}")
|
|
467
605
|
def close(): Unit = println("service closed")
|
|
468
606
|
}
|
|
469
607
|
|
|
470
|
-
//
|
|
608
|
+
// Only provide leaf values (primitives, configs) - the rest is auto-wired:
|
|
471
609
|
val serviceResource: Resource[Service] =
|
|
472
610
|
Resource.from[Service](
|
|
473
|
-
Wire(Config("jdbc:postgresql://localhost/db"))
|
|
474
|
-
Wire(new ConsoleLogger: Logger)
|
|
611
|
+
Wire(Config("jdbc:postgresql://localhost/db"))
|
|
475
612
|
)
|
|
476
613
|
|
|
477
614
|
Scope.global.scoped { scope =>
|
|
478
615
|
val svc = scope.allocate(serviceResource)
|
|
479
|
-
scope
|
|
616
|
+
(scope $ svc)(_.run())
|
|
480
617
|
}
|
|
618
|
+
// Output: running with [jdbc:postgresql://localhost/db] SELECT 1
|
|
619
|
+
// Then: service closed, database closed (LIFO order)
|
|
481
620
|
```
|
|
482
621
|
|
|
483
|
-
|
|
484
|
-
-
|
|
485
|
-
-
|
|
622
|
+
**What you must provide:**
|
|
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`)
|
|
486
629
|
|
|
487
630
|
---
|
|
488
631
|
|
|
489
|
-
###
|
|
632
|
+
### Injecting traits via subtype wires
|
|
490
633
|
|
|
491
|
-
When
|
|
634
|
+
When a dependency is a trait or abstract class, provide a wire for a concrete implementation:
|
|
492
635
|
|
|
493
636
|
```scala
|
|
494
637
|
import zio.blocks.scope._
|
|
495
|
-
import zio.blocks.context.Context
|
|
496
638
|
|
|
497
|
-
trait
|
|
498
|
-
def
|
|
639
|
+
trait Logger {
|
|
640
|
+
def info(msg: String): Unit
|
|
499
641
|
}
|
|
500
642
|
|
|
501
|
-
final class
|
|
502
|
-
def
|
|
503
|
-
def close(): Unit = println("LiveDatabaseApi closed")
|
|
643
|
+
final class ConsoleLogger extends Logger {
|
|
644
|
+
def info(msg: String): Unit = println(msg)
|
|
504
645
|
}
|
|
505
646
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
given Wireable.Typed[Config, DatabaseApi] =
|
|
509
|
-
Wireable.fromWire(shared[LiveDatabaseApi].shared.asInstanceOf[Wire[Config, DatabaseApi]])
|
|
647
|
+
final class App(logger: Logger) {
|
|
648
|
+
def run(): Unit = logger.info("Hello!")
|
|
510
649
|
}
|
|
511
650
|
|
|
512
|
-
|
|
651
|
+
// Wire.shared[ConsoleLogger] satisfies the Logger dependency via subtyping:
|
|
652
|
+
val appResource: Resource[App] =
|
|
653
|
+
Resource.from[App](
|
|
654
|
+
Wire.shared[ConsoleLogger]
|
|
655
|
+
)
|
|
513
656
|
|
|
514
657
|
Scope.global.scoped { scope =>
|
|
515
|
-
val
|
|
516
|
-
|
|
517
|
-
val db: DatabaseApi @@ scope.Tag =
|
|
518
|
-
scope.allocate(shared[DatabaseApi].toResource(deps))
|
|
519
|
-
|
|
520
|
-
val out: String =
|
|
521
|
-
scope.$(db)(_.query("SELECT 1"))
|
|
522
|
-
|
|
523
|
-
println(out)
|
|
658
|
+
val app = scope.allocate(appResource)
|
|
659
|
+
(scope $ app)(_.run())
|
|
524
660
|
}
|
|
525
661
|
```
|
|
526
662
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
663
|
+
**Single instance for diamond patterns:**
|
|
664
|
+
|
|
665
|
+
```scala
|
|
666
|
+
trait Service
|
|
667
|
+
class LiveService extends Service
|
|
668
|
+
class NeedsService(s: Service)
|
|
669
|
+
class NeedsLive(l: LiveService)
|
|
670
|
+
class App(a: NeedsService, b: NeedsLive)
|
|
671
|
+
|
|
672
|
+
// One LiveService instance satisfies both Service and LiveService dependencies:
|
|
673
|
+
val appResource = Resource.from[App](
|
|
674
|
+
Wire.shared[LiveService]
|
|
675
|
+
)
|
|
676
|
+
// count of LiveService instantiations: 1
|
|
677
|
+
```
|
|
530
678
|
|
|
531
679
|
---
|
|
532
680
|
|
|
@@ -549,6 +697,168 @@ Scope.global.scoped { scope =>
|
|
|
549
697
|
|
|
550
698
|
---
|
|
551
699
|
|
|
700
|
+
## Common compile errors
|
|
701
|
+
|
|
702
|
+
The scope macros produce beautiful, actionable compile-time error messages with ASCII diagrams and helpful hints:
|
|
703
|
+
|
|
704
|
+
### Not a class (Wire.shared/unique on trait or abstract class)
|
|
705
|
+
|
|
706
|
+
```
|
|
707
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
708
|
+
|
|
709
|
+
Cannot derive Wire for MyTrait: not a class.
|
|
710
|
+
|
|
711
|
+
Hint: Use Wire.Shared / Wire.Unique directly.
|
|
712
|
+
|
|
713
|
+
────────────────────────────────────────────────────────────────────────────
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
### Unmakeable type (primitives, functions, collections)
|
|
717
|
+
|
|
718
|
+
```
|
|
719
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
720
|
+
|
|
721
|
+
Cannot auto-create String
|
|
722
|
+
|
|
723
|
+
This type (primitive, collection, or function) cannot be auto-created.
|
|
724
|
+
|
|
725
|
+
Required by:
|
|
726
|
+
└── Config
|
|
727
|
+
└── App
|
|
728
|
+
|
|
729
|
+
Fix: Provide Wire(value) with the desired value:
|
|
730
|
+
|
|
731
|
+
Resource.from[...](
|
|
732
|
+
Wire(...), // provide a value for String
|
|
733
|
+
...
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
────────────────────────────────────────────────────────────────────────────
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### Abstract type (trait or abstract class)
|
|
740
|
+
|
|
741
|
+
```
|
|
742
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
743
|
+
|
|
744
|
+
Cannot auto-create Logger
|
|
745
|
+
|
|
746
|
+
This type is abstract (trait or abstract class).
|
|
747
|
+
|
|
748
|
+
Required by:
|
|
749
|
+
└── App
|
|
750
|
+
|
|
751
|
+
Fix: Provide a wire for a concrete implementation:
|
|
752
|
+
|
|
753
|
+
Resource.from[...](
|
|
754
|
+
Wire.shared[ConcreteImpl], // provides Logger
|
|
755
|
+
...
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
────────────────────────────────────────────────────────────────────────────
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Duplicate providers (ambiguous wires)
|
|
762
|
+
|
|
763
|
+
```
|
|
764
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
Multiple providers for Service
|
|
767
|
+
|
|
768
|
+
Conflicting wires:
|
|
769
|
+
1. LiveService
|
|
770
|
+
2. TestService
|
|
771
|
+
|
|
772
|
+
Hint: Remove duplicate wires or use distinct wrapper types.
|
|
773
|
+
|
|
774
|
+
────────────────────────────────────────────────────────────────────────────
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Dependency cycle
|
|
778
|
+
|
|
779
|
+
```
|
|
780
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
781
|
+
|
|
782
|
+
Dependency cycle detected
|
|
783
|
+
|
|
784
|
+
Cycle:
|
|
785
|
+
┌───────────┐
|
|
786
|
+
│ ▼
|
|
787
|
+
A ──► B ──► C
|
|
788
|
+
▲ │
|
|
789
|
+
└───────────┘
|
|
790
|
+
|
|
791
|
+
Break the cycle by:
|
|
792
|
+
• Introducing an interface/trait
|
|
793
|
+
• Using lazy initialization
|
|
794
|
+
• Restructuring dependencies
|
|
795
|
+
|
|
796
|
+
────────────────────────────────────────────────────────────────────────────
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
### Subtype conflict (related dependency types)
|
|
800
|
+
|
|
801
|
+
```
|
|
802
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
803
|
+
|
|
804
|
+
Dependency type conflict in MyService
|
|
805
|
+
|
|
806
|
+
FileInputStream is a subtype of InputStream.
|
|
807
|
+
|
|
808
|
+
When both types are dependencies, Context cannot reliably distinguish
|
|
809
|
+
them. The more specific type may be retrieved when the more general
|
|
810
|
+
type is requested.
|
|
811
|
+
|
|
812
|
+
To fix this, wrap one or both types in a distinct wrapper:
|
|
813
|
+
|
|
814
|
+
case class WrappedInputStream(value: InputStream)
|
|
815
|
+
or
|
|
816
|
+
opaque type WrappedInputStream = InputStream
|
|
817
|
+
|
|
818
|
+
────────────────────────────────────────────────────────────────────────────
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### Duplicate parameter types in constructor
|
|
822
|
+
|
|
823
|
+
```
|
|
824
|
+
── Scope Error ──────────────────────────────────────────────────────────────
|
|
825
|
+
|
|
826
|
+
Constructor of App has multiple parameters of type String
|
|
827
|
+
|
|
828
|
+
Context is type-indexed and cannot supply distinct values for the same type.
|
|
829
|
+
|
|
830
|
+
Fix: Wrap one parameter in an opaque type to distinguish them:
|
|
831
|
+
|
|
832
|
+
opaque type FirstString = String
|
|
833
|
+
or
|
|
834
|
+
case class FirstString(value: String)
|
|
835
|
+
|
|
836
|
+
────────────────────────────────────────────────────────────────────────────
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
### Leak warning
|
|
840
|
+
|
|
841
|
+
When using `leak(value)` to escape the scoped type system:
|
|
842
|
+
|
|
843
|
+
```
|
|
844
|
+
── Scope Warning ────────────────────────────────────────────────────────────
|
|
845
|
+
|
|
846
|
+
leak(db)
|
|
847
|
+
^
|
|
848
|
+
|
|
|
849
|
+
|
|
850
|
+
Warning: db is being leaked from scope MyScope.
|
|
851
|
+
This may result in undefined behavior.
|
|
852
|
+
|
|
853
|
+
Hint:
|
|
854
|
+
If you know this data type is not resourceful, then add an Unscoped
|
|
855
|
+
instance for it so ScopeLift can lift it automatically.
|
|
856
|
+
|
|
857
|
+
────────────────────────────────────────────────────────────────────────────
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
552
862
|
## API reference (selected)
|
|
553
863
|
|
|
554
864
|
### `Scope`
|
|
@@ -562,18 +872,16 @@ final class Scope[ParentTag, Tag0 <: ParentTag] {
|
|
|
562
872
|
def allocate[A](resource: Resource[A]): A @@ Tag
|
|
563
873
|
def defer(f: => Unit): Unit
|
|
564
874
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
escape: ScopeEscape[B, S]
|
|
568
|
-
): escape.Out
|
|
875
|
+
// Apply function to scoped value - executes immediately, always returns scoped
|
|
876
|
+
def $[A, B](scoped: A @@ this.Tag)(f: A => B): B @@ Tag
|
|
569
877
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
escape: ScopeEscape[A, S]
|
|
573
|
-
): escape.Out
|
|
878
|
+
// Execute scoped computation - runs immediately, always returns scoped
|
|
879
|
+
def execute[A](scoped: A @@ this.Tag): A @@ Tag
|
|
574
880
|
|
|
575
|
-
// Creates a child scope
|
|
576
|
-
def scoped[A](f: Scope[this.Tag, ? <: this.Tag] => A)
|
|
881
|
+
// Creates a child scope - ScopeLift controls what may exit
|
|
882
|
+
def scoped[A](f: Scope[this.Tag, ? <: this.Tag] => A)(
|
|
883
|
+
using lift: ScopeLift[A, this.Tag]
|
|
884
|
+
): lift.Out
|
|
577
885
|
}
|
|
578
886
|
```
|
|
579
887
|
|
|
@@ -587,20 +895,16 @@ object Resource {
|
|
|
587
895
|
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
|
|
588
896
|
def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
|
|
589
897
|
|
|
590
|
-
// Macro-
|
|
591
|
-
def from[T]: Resource[T]
|
|
898
|
+
// Macro - DI entry point (can also be called with no args for zero-dep classes):
|
|
592
899
|
def from[T](wires: Wire[?, ?]*): Resource[T]
|
|
593
900
|
|
|
594
|
-
// Internal
|
|
595
|
-
def shared[A](f: Finalizer => A): Resource
|
|
596
|
-
def unique[A](f: Finalizer => A): Resource
|
|
597
|
-
|
|
598
|
-
final class Shared[+A] extends Resource[A]
|
|
599
|
-
final class Unique[+A] extends Resource[A]
|
|
901
|
+
// Internal (used by generated code):
|
|
902
|
+
def shared[A](f: Finalizer => A): Resource[A]
|
|
903
|
+
def unique[A](f: Finalizer => A): Resource[A]
|
|
600
904
|
}
|
|
601
905
|
```
|
|
602
906
|
|
|
603
|
-
### `Wire`
|
|
907
|
+
### `Wire`
|
|
604
908
|
|
|
605
909
|
```scala
|
|
606
910
|
sealed trait Wire[-In, +Out] {
|
|
@@ -610,9 +914,16 @@ sealed trait Wire[-In, +Out] {
|
|
|
610
914
|
def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
|
|
611
915
|
}
|
|
612
916
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
def
|
|
917
|
+
object Wire {
|
|
918
|
+
// Macro entry points:
|
|
919
|
+
def shared[T]: Wire[?, T] // derive from T's constructor
|
|
920
|
+
def unique[T]: Wire[?, T] // derive from T's constructor
|
|
921
|
+
|
|
922
|
+
// Wrap pre-existing value (auto-finalizes if AutoCloseable):
|
|
923
|
+
def apply[T](value: T): Wire.Shared[Any, T]
|
|
924
|
+
|
|
925
|
+
final class Shared[-In, +Out] extends Wire[In, Out]
|
|
926
|
+
final class Unique[-In, +Out] extends Wire[In, Out]
|
|
616
927
|
}
|
|
617
928
|
```
|
|
618
929
|
|
|
@@ -621,7 +932,16 @@ trait Wireable[+Out] {
|
|
|
621
932
|
## Mental model recap
|
|
622
933
|
|
|
623
934
|
- Use `Scope.global.scoped { scope => ... }` to create a safe region.
|
|
624
|
-
-
|
|
625
|
-
-
|
|
935
|
+
- For simple resources: `scope.allocate(Resource(value))` or `scope.allocate(Resource.acquireRelease(...)(...))`
|
|
936
|
+
- For dependency injection: `scope.allocate(Resource.from[App](Wire(config), ...))` — auto-wires concrete classes, you provide leaves and overrides.
|
|
937
|
+
- Use scoped values via `(scope $ value) { v => ... }` — the function executes immediately.
|
|
938
|
+
- `$` and `execute` always return scoped values (`B @@ scope.Tag`), never raw values.
|
|
939
|
+
- Escape only happens at `.scoped` boundaries via `ScopeLift`.
|
|
626
940
|
- Nest with `scope.scoped { child => ... }` to create a tighter lifetime boundary.
|
|
941
|
+
- Return `Unscoped` types from child scopes to extract raw values.
|
|
627
942
|
- If it doesn't typecheck, it would have been unsafe at runtime.
|
|
943
|
+
|
|
944
|
+
**The 3 macro entry points:**
|
|
945
|
+
- `Wire.shared[T]` — shared wire from constructor
|
|
946
|
+
- `Wire.unique[T]` — unique wire from constructor
|
|
947
|
+
- `Resource.from[T](wires*)` — wire up T and all dependencies
|