@zio.dev/zio-blocks 0.0.1
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 +426 -0
- package/package.json +6 -0
- package/path-interpolator.md +645 -0
- package/reference/binding.md +364 -0
- package/reference/chunk.md +576 -0
- package/reference/context.md +157 -0
- package/reference/docs.md +524 -0
- package/reference/dynamic-value.md +823 -0
- package/reference/formats.md +640 -0
- package/reference/json-schema.md +626 -0
- package/reference/json.md +979 -0
- package/reference/modifier.md +276 -0
- package/reference/optics.md +1613 -0
- package/reference/patch.md +631 -0
- package/reference/reflect-transform.md +387 -0
- package/reference/reflect.md +521 -0
- package/reference/registers.md +282 -0
- package/reference/schema-evolution.md +540 -0
- package/reference/schema.md +619 -0
- package/reference/syntax.md +409 -0
- package/reference/type-class-derivation-internals.md +632 -0
- package/reference/typeid.md +900 -0
- package/reference/validation.md +458 -0
- package/scope.md +627 -0
- package/sidebars.js +30 -0
package/scope.md
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
# ZIO Blocks — Scope (compile-time safe resource management)
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
If you've used `try/finally`, `Using`, or ZIO `Scope`, this library lives in the same problem space, but it focuses on:
|
|
6
|
+
|
|
7
|
+
- **Compile-time prevention of scope leaks**
|
|
8
|
+
- **Zero runtime overhead for the scoped tag** (`A @@ S` is represented as `A`)
|
|
9
|
+
- **Simple, synchronous lifecycle management** (finalizers run LIFO on scope close)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of contents
|
|
14
|
+
|
|
15
|
+
- [Quick start](#quick-start)
|
|
16
|
+
- [Core concepts](#core-concepts)
|
|
17
|
+
- [1) `Scope[ParentTag, Tag]`](#1-scopeparenttag-tag)
|
|
18
|
+
- [2) Scoped values: `A @@ S`](#2-scoped-values-a--s)
|
|
19
|
+
- [3) `Resource[A]`: acquisition + finalization](#3-resourcea-acquisition--finalization)
|
|
20
|
+
- [4) `Scoped[Tag, A]`: deferred computations](#4-scopedtag-a-deferred-computations)
|
|
21
|
+
- [5) `ScopeEscape` and `Unscoped`: what may escape](#5-scopeescape-and-unscoped-what-may-escape)
|
|
22
|
+
- [6) `Wire[-In, +Out]`: dependency recipes](#6-wire-in-out-dependency-recipes)
|
|
23
|
+
- [7) `Wireable[Out]`: DI for traits/abstract classes](#7-wireableout-di-for-traitsabstract-classes)
|
|
24
|
+
- [Safety model (why leaking is prevented)](#safety-model-why-leaking-is-prevented)
|
|
25
|
+
- [Usage examples](#usage-examples)
|
|
26
|
+
- [Allocating and using a resource](#allocating-and-using-a-resource)
|
|
27
|
+
- [Nested scopes (child can use parent, not vice versa)](#nested-scopes-child-can-use-parent-not-vice-versa)
|
|
28
|
+
- [Building a `Scoped` program (map/flatMap)](#building-a-scoped-program-mapflatmap)
|
|
29
|
+
- [Registering cleanup manually with `defer`](#registering-cleanup-manually-with-defer)
|
|
30
|
+
- [Dependency injection with `Wire` + `Context`](#dependency-injection-with-wire--context)
|
|
31
|
+
- [Supplying dependencies with `Resource.from[T](wire1, wire2, ...)`](#supplying-dependencies-with-resourcefromtwire1-wire2-)
|
|
32
|
+
- [DI for traits via `Wireable`](#di-for-traits-via-wireable)
|
|
33
|
+
- [Interop escape hatch: `leak`](#interop-escape-hatch-leak)
|
|
34
|
+
- [API reference (selected)](#api-reference-selected)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```scala
|
|
41
|
+
import zio.blocks.scope._
|
|
42
|
+
|
|
43
|
+
final class Database extends AutoCloseable {
|
|
44
|
+
def query(sql: String): String = s"result: $sql"
|
|
45
|
+
def close(): Unit = println("db closed")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
Scope.global.scoped { scope =>
|
|
49
|
+
val db: Database @@ scope.Tag =
|
|
50
|
+
scope.allocate(Resource(new Database))
|
|
51
|
+
|
|
52
|
+
val result: String =
|
|
53
|
+
scope.$(db)(_.query("SELECT 1"))
|
|
54
|
+
|
|
55
|
+
println(result)
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Key things to notice:
|
|
60
|
+
|
|
61
|
+
- `scope.allocate(...)` returns a **scoped** value: `Database @@ scope.Tag`
|
|
62
|
+
- You **cannot** call `db.query(...)` directly (methods are intentionally hidden)
|
|
63
|
+
- You must use `scope.$(db)(...)` or build a `Scoped` computation
|
|
64
|
+
- When the `scoped { ... }` block exits, finalizers run **LIFO** and errors are handled safely
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Core concepts
|
|
69
|
+
|
|
70
|
+
### 1) `Scope[ParentTag, Tag]`
|
|
71
|
+
|
|
72
|
+
A `Scope` manages finalizers and ties values to a *type-level identity* called a **Tag**.
|
|
73
|
+
|
|
74
|
+
- `Scope[ParentTag, Tag]` has **two** type parameters:
|
|
75
|
+
- `ParentTag`: the parent scope's tag (capability boundary)
|
|
76
|
+
- `Tag <: ParentTag`: this scope's unique identity (used to tag values)
|
|
77
|
+
|
|
78
|
+
Every `Scope` also exposes a *path-dependent* member type:
|
|
79
|
+
|
|
80
|
+
```scala
|
|
81
|
+
type Tag = Tag0
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
So in code you'll typically write:
|
|
85
|
+
|
|
86
|
+
```scala
|
|
87
|
+
Scope.global.scoped { scope =>
|
|
88
|
+
val x: Something @@ scope.Tag = ???
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
#### Global scope
|
|
93
|
+
|
|
94
|
+
`Scope.global` is the root of the tag hierarchy:
|
|
95
|
+
|
|
96
|
+
```scala
|
|
97
|
+
object Scope {
|
|
98
|
+
type GlobalTag
|
|
99
|
+
lazy val global: Scope[GlobalTag, GlobalTag]
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- The global scope is intended to live for the lifetime of the process.
|
|
104
|
+
- Its finalizers run on JVM shutdown.
|
|
105
|
+
- Values allocated in `Scope.global` typically **escape** as raw values via `ScopeEscape` (see below).
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### 2) Scoped values: `A @@ S`
|
|
110
|
+
|
|
111
|
+
`A @@ S` means: "a value of type `A` that is locked to scope tag `S`".
|
|
112
|
+
|
|
113
|
+
- **Runtime representation:** just `A` (no wrapper allocation)
|
|
114
|
+
- **Key effect:** methods on `A` are hidden, so you can't call `a.method` without proving scope access
|
|
115
|
+
- **Access paths:**
|
|
116
|
+
- `scope.$(a)(f)` to use a scoped value immediately
|
|
117
|
+
- `a.map / a.flatMap` to build a `Scoped` computation, then run it via `scope(scoped)`
|
|
118
|
+
|
|
119
|
+
#### Scala 3 vs Scala 2 note
|
|
120
|
+
|
|
121
|
+
- In **Scala 3**, `@@` is implemented as an `opaque` type.
|
|
122
|
+
- In **Scala 2**, the library emulates the same "opaque-like" behavior using the *module pattern* (still zero-overhead at runtime).
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### 3) `Resource[A]`: acquisition + finalization
|
|
127
|
+
|
|
128
|
+
`Resource[A]` describes how to **acquire** an `A` and how to **release** it when a scope closes. It is intentionally lazy: you *describe what to do*, and allocation happens only through:
|
|
129
|
+
|
|
130
|
+
```scala
|
|
131
|
+
scope.allocate(resource)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Common constructors:
|
|
135
|
+
|
|
136
|
+
- `Resource(a)`
|
|
137
|
+
- Wraps a by-name value; if it's `AutoCloseable`, `close()` is registered automatically.
|
|
138
|
+
- `Resource.acquireRelease(acquire)(release)`
|
|
139
|
+
- Explicit lifecycle.
|
|
140
|
+
- `Resource.fromAutoCloseable(thunk)`
|
|
141
|
+
- A type-safe helper for `AutoCloseable`.
|
|
142
|
+
- `Resource.from[T]` (macro)
|
|
143
|
+
- Derives a resource from `T`'s constructor.
|
|
144
|
+
- If `T` is `AutoCloseable`, registers `close()` automatically.
|
|
145
|
+
- If `T` has dependencies, either:
|
|
146
|
+
- use `Wire` + `toResource(deps)`, or
|
|
147
|
+
- use `Resource.from[T](wire1, wire2, ...)` (see below).
|
|
148
|
+
|
|
149
|
+
#### Resource "sharing" vs "uniqueness"
|
|
150
|
+
|
|
151
|
+
`Resource` has two important internal flavors:
|
|
152
|
+
|
|
153
|
+
- `Resource.Unique[A]`
|
|
154
|
+
- Produces a **fresh** instance every time you allocate it (typical for `Resource(...)`, `acquireRelease`, etc.).
|
|
155
|
+
- `Resource.Shared[A]`
|
|
156
|
+
- Produces a **shared** instance per `Resource.Shared` value, with **reference counting**:
|
|
157
|
+
- the first allocation initializes the value and collects finalizers
|
|
158
|
+
- each allocating scope registers a decrement finalizer
|
|
159
|
+
- when the reference count reaches zero, the collected finalizers run
|
|
160
|
+
|
|
161
|
+
**Important clarification:** sharing is **not** "memoized within a Wire graph" or "within a scope" by magic. Sharing happens **within the specific `Resource.Shared` instance** you reuse.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### 4) `Scoped[Tag, A]`: deferred computations
|
|
166
|
+
|
|
167
|
+
`Scoped[-Tag, +A]` represents a computation that produces `A`, but can only be executed by a scope whose tag is compatible with `Tag`.
|
|
168
|
+
|
|
169
|
+
Execution happens via:
|
|
170
|
+
|
|
171
|
+
```scala
|
|
172
|
+
scope(scopedComputation)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
How to build them:
|
|
176
|
+
|
|
177
|
+
- From scoped values:
|
|
178
|
+
- `val s: Scoped[S, B] = (a: A @@ S).map(f)`
|
|
179
|
+
- `flatMap` composes scoped values while tracking combined requirements
|
|
180
|
+
- Or directly:
|
|
181
|
+
- `Scoped.create(() => ...)` (advanced/internal style)
|
|
182
|
+
|
|
183
|
+
`Scoped` is contravariant in `Tag`, which is what allows a **child** scope (more specific tag) to run computations that only require a **parent** tag.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### 5) `ScopeEscape` and `Unscoped`: what may escape
|
|
188
|
+
|
|
189
|
+
Whenever you access a scoped value via:
|
|
190
|
+
|
|
191
|
+
- `scope.$(value)(f)`, or
|
|
192
|
+
- `scope(scopedComputation)`,
|
|
193
|
+
|
|
194
|
+
…the return type is controlled by `ScopeEscape[A, S]`, which decides whether a result:
|
|
195
|
+
|
|
196
|
+
- escapes as raw `A`, or
|
|
197
|
+
- remains tracked as `A @@ S`.
|
|
198
|
+
|
|
199
|
+
Rule of thumb:
|
|
200
|
+
|
|
201
|
+
- Pure data (e.g. `Int`, `String`, small case classes you mark `Unscoped`) should escape as raw values.
|
|
202
|
+
- Resource-like values should remain scoped unless you explicitly `leak`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### 6) `Wire[-In, +Out]`: dependency recipes
|
|
207
|
+
|
|
208
|
+
`Wire` is a recipe for constructing services, commonly used for dependency injection.
|
|
209
|
+
|
|
210
|
+
- `In` is the required dependencies (provided as a `Context[In]`)
|
|
211
|
+
- `Out` is the produced service
|
|
212
|
+
|
|
213
|
+
There are two wire flavors:
|
|
214
|
+
|
|
215
|
+
- `Wire.Shared`: a shared recipe
|
|
216
|
+
- `Wire.Unique`: a unique recipe
|
|
217
|
+
|
|
218
|
+
**Important clarification:** `Wire` itself is just a recipe. The actual memoization/sharing behavior happens when you convert the wire into a `Resource`:
|
|
219
|
+
|
|
220
|
+
```scala
|
|
221
|
+
val r: Resource[Out] = wire.toResource(deps)
|
|
222
|
+
val out: Out @@ scope.Tag = scope.allocate(r)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- `Wire.Shared#toResource` produces a `Resource.Shared`, which is where the reference-counted sharing is implemented.
|
|
226
|
+
- `Wire.Unique#toResource` produces a `Resource.Unique`.
|
|
227
|
+
|
|
228
|
+
Macros available at package level:
|
|
229
|
+
|
|
230
|
+
- `shared[T]`: derive a shared wire from `T`'s constructor (or from a `Wireable[T]` if present)
|
|
231
|
+
- `unique[T]`: derive a unique wire
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### 7) `Wireable[Out]`: DI for traits/abstract classes
|
|
236
|
+
|
|
237
|
+
`shared[T]` / `unique[T]` can derive wires from **concrete classes** with constructors. But traits and abstract classes are not instantiable, so you need a way to tell the macros "when someone asks for `T`, build it like *this*".
|
|
238
|
+
|
|
239
|
+
That's what `Wireable[T]` is: a typeclass that supplies a `Wire` for a service.
|
|
240
|
+
|
|
241
|
+
Typical use:
|
|
242
|
+
|
|
243
|
+
- Define a `Wireable[MyTrait]` in `MyTrait`'s companion object.
|
|
244
|
+
- `shared[MyTrait]` or `unique[MyTrait]` will pick it up automatically.
|
|
245
|
+
|
|
246
|
+
This is especially useful when you want to inject an interface but construct a concrete implementation (and still register finalizers correctly).
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Safety model (why leaking is prevented)
|
|
251
|
+
|
|
252
|
+
The library prevents scope leaks via two reinforcing mechanisms:
|
|
253
|
+
|
|
254
|
+
### A) Existential child tags (fresh, unnameable types)
|
|
255
|
+
|
|
256
|
+
Child scopes are created with:
|
|
257
|
+
|
|
258
|
+
```scala
|
|
259
|
+
Scope.global.scoped { scope =>
|
|
260
|
+
scope.scoped { child =>
|
|
261
|
+
// allocate in child
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The child scope has an existential tag (fresh per invocation). You can allocate in the child, but you can't return those values to the parent in a usable form because the parent cannot name (or satisfy) the child tag.
|
|
267
|
+
|
|
268
|
+
Compile-time safety is verified in tests, e.g.:
|
|
269
|
+
`ScopeCompileTimeSafetyScala3Spec`.
|
|
270
|
+
|
|
271
|
+
### B) Tag invariance + "opaque-like" `@@` blocks subtyping escape
|
|
272
|
+
|
|
273
|
+
Even if you try to "widen" a child-tagged value to a parent-tagged value, invariance and hidden members prevent it from typechecking. The only sanctioned access route is through `scope.$` / `scope.apply`, which require tag evidence.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Usage examples
|
|
278
|
+
|
|
279
|
+
### Allocating and using a resource
|
|
280
|
+
|
|
281
|
+
```scala
|
|
282
|
+
import zio.blocks.scope._
|
|
283
|
+
|
|
284
|
+
final class FileHandle(path: String) extends AutoCloseable {
|
|
285
|
+
def readAll(): String = s"contents of $path"
|
|
286
|
+
def close(): Unit = println(s"closed $path")
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
Scope.global.scoped { scope =>
|
|
290
|
+
val h = scope.allocate(Resource(new FileHandle("data.txt")))
|
|
291
|
+
|
|
292
|
+
val contents: String =
|
|
293
|
+
scope.$(h)(_.readAll())
|
|
294
|
+
|
|
295
|
+
println(contents)
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
### Nested scopes (child can use parent, not vice versa)
|
|
302
|
+
|
|
303
|
+
```scala
|
|
304
|
+
import zio.blocks.scope._
|
|
305
|
+
|
|
306
|
+
Scope.global.scoped { parent =>
|
|
307
|
+
val parentDb = parent.allocate(Resource(new Database))
|
|
308
|
+
|
|
309
|
+
parent.scoped { child =>
|
|
310
|
+
// child can use parent-scoped values:
|
|
311
|
+
val ok: String = child.$(parentDb)(_.query("SELECT 1"))
|
|
312
|
+
println(ok)
|
|
313
|
+
|
|
314
|
+
val childDb = child.allocate(Resource(new Database))
|
|
315
|
+
|
|
316
|
+
// You can use childDb *inside* the child:
|
|
317
|
+
val ok2: String = child.$(childDb)(_.query("SELECT 2"))
|
|
318
|
+
println(ok2)
|
|
319
|
+
|
|
320
|
+
// But you cannot return childDb to the parent in a usable way:
|
|
321
|
+
// childDb : Database @@ child.Tag
|
|
322
|
+
// parent cannot prove parent.Tag <:< child.Tag
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// parentDb is still usable here:
|
|
326
|
+
val stillOk = parent.$(parentDb)(_.query("SELECT 3"))
|
|
327
|
+
println(stillOk)
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### Building a `Scoped` program (map/flatMap)
|
|
334
|
+
|
|
335
|
+
```scala
|
|
336
|
+
import zio.blocks.scope._
|
|
337
|
+
|
|
338
|
+
Scope.global.scoped { scope =>
|
|
339
|
+
val db = scope.allocate(Resource(new Database))
|
|
340
|
+
|
|
341
|
+
val program: Scoped[scope.Tag, String] =
|
|
342
|
+
for {
|
|
343
|
+
a <- db.map(_.query("SELECT 1"))
|
|
344
|
+
b <- db.map(_.query("SELECT 2"))
|
|
345
|
+
} yield s"$a | $b"
|
|
346
|
+
|
|
347
|
+
val result: String = scope(program)
|
|
348
|
+
println(result)
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### Registering cleanup manually with `defer`
|
|
355
|
+
|
|
356
|
+
Use `scope.defer` when you already have a value and just need to register cleanup.
|
|
357
|
+
|
|
358
|
+
```scala
|
|
359
|
+
import zio.blocks.scope._
|
|
360
|
+
|
|
361
|
+
Scope.global.scoped { scope =>
|
|
362
|
+
val handle = new java.io.ByteArrayInputStream(Array[Byte](1, 2, 3))
|
|
363
|
+
|
|
364
|
+
scope.defer { handle.close() }
|
|
365
|
+
|
|
366
|
+
val firstByte = handle.read()
|
|
367
|
+
println(firstByte)
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
There is also a package-level helper `defer` that only requires a `Finalizer`:
|
|
372
|
+
|
|
373
|
+
```scala
|
|
374
|
+
import zio.blocks.scope._
|
|
375
|
+
|
|
376
|
+
Scope.global.scoped { scope =>
|
|
377
|
+
given Finalizer = scope
|
|
378
|
+
defer { println("cleanup") }
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
### Classes with `Finalizer` parameters
|
|
385
|
+
|
|
386
|
+
If your class needs to register cleanup logic, accept a `Finalizer` parameter (not `Scope`). The wire and resource macros automatically inject the `Finalizer` when constructing such classes.
|
|
387
|
+
|
|
388
|
+
```scala
|
|
389
|
+
import zio.blocks.scope._
|
|
390
|
+
|
|
391
|
+
class ConnectionPool(config: Config)(implicit finalizer: Finalizer) {
|
|
392
|
+
private val pool = createPool(config)
|
|
393
|
+
finalizer.defer { pool.shutdown() }
|
|
394
|
+
|
|
395
|
+
def getConnection(): Connection = pool.acquire()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// The macro sees the implicit Finalizer and injects it automatically:
|
|
399
|
+
val resource = Resource.from[ConnectionPool](Wire(Config("jdbc://localhost")))
|
|
400
|
+
|
|
401
|
+
Scope.global.scoped { scope =>
|
|
402
|
+
val pool = scope.allocate(resource)
|
|
403
|
+
// pool.shutdown() will be called when scope closes
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Why `Finalizer` instead of `Scope`?
|
|
408
|
+
- `Finalizer` is the minimal interface—it only has `defer`
|
|
409
|
+
- Classes that need cleanup should not have access to `allocate` or `$`
|
|
410
|
+
- The macros pass a `Finalizer` at runtime, so declaring `Scope` would be misleading
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### Dependency injection with `Wire` + `Context`
|
|
415
|
+
|
|
416
|
+
```scala
|
|
417
|
+
import zio.blocks.scope._
|
|
418
|
+
import zio.blocks.context.Context
|
|
419
|
+
|
|
420
|
+
final case class Config(debug: Boolean)
|
|
421
|
+
|
|
422
|
+
val w: Wire.Shared[Boolean, Config] = shared[Config]
|
|
423
|
+
val deps: Context[Boolean] = Context[Boolean](true)
|
|
424
|
+
|
|
425
|
+
Scope.global.scoped { scope =>
|
|
426
|
+
val cfg: Config @@ scope.Tag =
|
|
427
|
+
scope.allocate(w.toResource(deps))
|
|
428
|
+
|
|
429
|
+
val debug: Boolean =
|
|
430
|
+
scope.$(cfg)(_.debug) // Boolean typically escapes
|
|
431
|
+
|
|
432
|
+
println(debug)
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Sharing vs uniqueness at the wire level:
|
|
437
|
+
|
|
438
|
+
```scala
|
|
439
|
+
import zio.blocks.scope._
|
|
440
|
+
|
|
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
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
### Supplying dependencies with `Resource.from[T](wire1, wire2, ...)`
|
|
448
|
+
|
|
449
|
+
`Resource.from[T]` can also be used as a "standalone mini graph" by providing wires for constructor dependencies.
|
|
450
|
+
|
|
451
|
+
```scala
|
|
452
|
+
import zio.blocks.scope._
|
|
453
|
+
import zio.blocks.context.Context
|
|
454
|
+
|
|
455
|
+
final case class Config(url: String)
|
|
456
|
+
|
|
457
|
+
trait Logger {
|
|
458
|
+
def info(msg: String): Unit
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
final class ConsoleLogger extends Logger {
|
|
462
|
+
def info(msg: String): Unit = println(msg)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
final class Service(cfg: Config, logger: Logger) extends AutoCloseable {
|
|
466
|
+
def run(): Unit = logger.info(s"running with ${cfg.url}")
|
|
467
|
+
def close(): Unit = println("service closed")
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Provide wires for *all* dependencies of Service:
|
|
471
|
+
val serviceResource: Resource[Service] =
|
|
472
|
+
Resource.from[Service](
|
|
473
|
+
Wire(Config("jdbc:postgresql://localhost/db")),
|
|
474
|
+
Wire(new ConsoleLogger: Logger)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
Scope.global.scoped { scope =>
|
|
478
|
+
val svc = scope.allocate(serviceResource)
|
|
479
|
+
scope.$(svc)(_.run())
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Notes:
|
|
484
|
+
- All dependencies of `T` must be covered by the provided wires, otherwise you get a compile-time error.
|
|
485
|
+
- If `T` is `AutoCloseable`, `close()` is registered automatically.
|
|
486
|
+
|
|
487
|
+
---
|
|
488
|
+
|
|
489
|
+
### DI for traits via `Wireable`
|
|
490
|
+
|
|
491
|
+
When you want to inject a trait (or abstract class), define a `Wireable` in the companion so `shared[T]` / `unique[T]` can resolve it.
|
|
492
|
+
|
|
493
|
+
```scala
|
|
494
|
+
import zio.blocks.scope._
|
|
495
|
+
import zio.blocks.context.Context
|
|
496
|
+
|
|
497
|
+
trait DatabaseApi {
|
|
498
|
+
def query(sql: String): String
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
final class LiveDatabaseApi(cfg: Config) extends DatabaseApi with AutoCloseable {
|
|
502
|
+
def query(sql: String): String = s"[${cfg.url}] $sql"
|
|
503
|
+
def close(): Unit = println("LiveDatabaseApi closed")
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
object DatabaseApi {
|
|
507
|
+
// Tell Scope how to build the trait by wiring a concrete implementation.
|
|
508
|
+
given Wireable.Typed[Config, DatabaseApi] =
|
|
509
|
+
Wireable.fromWire(shared[LiveDatabaseApi].shared.asInstanceOf[Wire[Config, DatabaseApi]])
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
final case class Config(url: String)
|
|
513
|
+
|
|
514
|
+
Scope.global.scoped { scope =>
|
|
515
|
+
val deps = Context(Config("jdbc:postgresql://localhost/db"))
|
|
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)
|
|
524
|
+
}
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Practical guidance:
|
|
528
|
+
- Prefer `Wireable.fromWire(...)` when you already have a `Wire` you trust.
|
|
529
|
+
- Put `given Wireable[...]` / `implicit val wireable: Wireable[...]` in the companion of the trait being injected.
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
### Interop escape hatch: `leak`
|
|
534
|
+
|
|
535
|
+
Sometimes you must hand a raw value to code that cannot work with `@@` types.
|
|
536
|
+
|
|
537
|
+
```scala
|
|
538
|
+
import zio.blocks.scope._
|
|
539
|
+
|
|
540
|
+
Scope.global.scoped { scope =>
|
|
541
|
+
val db = scope.allocate(Resource(new Database))
|
|
542
|
+
|
|
543
|
+
val raw: Database = leak(db) // emits a compiler warning
|
|
544
|
+
// thirdParty(raw)
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**Warning:** leaking bypasses compile-time guarantees. The value may be used after its scope closes. Use only when unavoidable.
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## API reference (selected)
|
|
553
|
+
|
|
554
|
+
### `Scope`
|
|
555
|
+
|
|
556
|
+
Core methods (Scala 3 `using` vs Scala 2 `implicit` differs, but the shapes are the same):
|
|
557
|
+
|
|
558
|
+
```scala
|
|
559
|
+
final class Scope[ParentTag, Tag0 <: ParentTag] {
|
|
560
|
+
type Tag = Tag0
|
|
561
|
+
|
|
562
|
+
def allocate[A](resource: Resource[A]): A @@ Tag
|
|
563
|
+
def defer(f: => Unit): Unit
|
|
564
|
+
|
|
565
|
+
def $[A, B, S](scoped: A @@ S)(f: A => B)(
|
|
566
|
+
using ev: this.Tag <:< S,
|
|
567
|
+
escape: ScopeEscape[B, S]
|
|
568
|
+
): escape.Out
|
|
569
|
+
|
|
570
|
+
def apply[A, S](scoped: Scoped[S, A])(
|
|
571
|
+
using ev: this.Tag <:< S,
|
|
572
|
+
escape: ScopeEscape[A, S]
|
|
573
|
+
): escape.Out
|
|
574
|
+
|
|
575
|
+
// Creates a child scope with an existential tag (fresh per call)
|
|
576
|
+
def scoped[A](f: Scope[this.Tag, ? <: this.Tag] => A): A
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### `Resource`
|
|
581
|
+
|
|
582
|
+
```scala
|
|
583
|
+
sealed trait Resource[+A]
|
|
584
|
+
|
|
585
|
+
object Resource {
|
|
586
|
+
def apply[A](value: => A): Resource[A]
|
|
587
|
+
def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
|
|
588
|
+
def fromAutoCloseable[A <: AutoCloseable](thunk: => A): Resource[A]
|
|
589
|
+
|
|
590
|
+
// Macro-derived constructors:
|
|
591
|
+
def from[T]: Resource[T]
|
|
592
|
+
def from[T](wires: Wire[?, ?]*): Resource[T]
|
|
593
|
+
|
|
594
|
+
// Internal / produced by wires:
|
|
595
|
+
def shared[A](f: Finalizer => A): Resource.Shared[A]
|
|
596
|
+
def unique[A](f: Finalizer => A): Resource.Unique[A]
|
|
597
|
+
|
|
598
|
+
final class Shared[+A] extends Resource[A]
|
|
599
|
+
final class Unique[+A] extends Resource[A]
|
|
600
|
+
}
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### `Wire` and `Wireable`
|
|
604
|
+
|
|
605
|
+
```scala
|
|
606
|
+
sealed trait Wire[-In, +Out] {
|
|
607
|
+
def isShared: Boolean
|
|
608
|
+
def shared: Wire.Shared[In, Out]
|
|
609
|
+
def unique: Wire.Unique[In, Out]
|
|
610
|
+
def toResource(deps: zio.blocks.context.Context[In]): Resource[Out]
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
trait Wireable[+Out] {
|
|
614
|
+
type In
|
|
615
|
+
def wire: Wire[In, Out]
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
## Mental model recap
|
|
622
|
+
|
|
623
|
+
- Use `Scope.global.scoped { scope => ... }` to create a safe region.
|
|
624
|
+
- Allocate managed things with `scope.allocate(Resource(...))` (or `Resource.from[...]`).
|
|
625
|
+
- Use scoped values only via `scope.$(value)(...)` or via `Scoped` computations executed by `scope(scoped)`.
|
|
626
|
+
- Nest with `scope.scoped { child => ... }` to create a tighter lifetime boundary.
|
|
627
|
+
- If it doesn't typecheck, it would have been unsafe at runtime.
|
package/sidebars.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const sidebars = {
|
|
2
|
+
sidebar: [
|
|
3
|
+
{
|
|
4
|
+
type: "category",
|
|
5
|
+
label: "ZIO Blocks",
|
|
6
|
+
collapsed: false,
|
|
7
|
+
link: { type: "doc", id: "index" },
|
|
8
|
+
items: [
|
|
9
|
+
"reference/schema",
|
|
10
|
+
"reference/reflect",
|
|
11
|
+
"reference/binding",
|
|
12
|
+
"reference/registers",
|
|
13
|
+
"reference/typeid",
|
|
14
|
+
"reference/dynamic-value",
|
|
15
|
+
"reference/optics",
|
|
16
|
+
"reference/chunk",
|
|
17
|
+
"reference/validation",
|
|
18
|
+
"reference/schema-evolution",
|
|
19
|
+
"reference/context",
|
|
20
|
+
"reference/docs",
|
|
21
|
+
"reference/formats",
|
|
22
|
+
"reference/json",
|
|
23
|
+
"reference/json-schema",
|
|
24
|
+
"reference/syntax",
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
module.exports = sidebars;
|