@zio.dev/zio-blocks 0.0.29 → 0.0.30
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/compile-time-resource-safety-with-scope.md +997 -0
- package/guides/query-dsl-extending.md +1 -1
- package/guides/query-dsl-fluent-builder.md +203 -203
- package/guides/query-dsl-reified-optics.md +1 -1
- package/guides/query-dsl-sql.md +1 -1
- package/guides/zio-schema-migration.md +6 -6
- package/index.md +20 -14
- package/package.json +1 -1
- package/reference/codec.md +7 -7
- package/reference/context.md +1 -1
- package/reference/docs.md +1 -1
- package/reference/dynamic-schema.md +39 -42
- package/reference/media-type.md +2 -2
- package/reference/resource-management/defer-handle.md +246 -0
- package/reference/resource-management/finalization.md +286 -0
- package/reference/resource-management/finalizer.md +167 -0
- package/reference/{resource-management-di → resource-management}/index.md +3 -8
- package/reference/{resource-management-di → resource-management}/resource.md +532 -50
- package/reference/resource-management/scope.md +3026 -0
- package/reference/resource-management/unscoped.md +125 -0
- package/reference/{resource-management-di → resource-management}/wire.md +74 -28
- package/reference/schema-evolution/as.md +4 -4
- package/reference/schema-evolution/into.md +2 -2
- package/reference/schema-expr.md +2 -2
- package/reference/streams.md +989 -0
- package/reference/type-class-derivation.md +1 -1
- package/ringbuffer.md +1 -1
- package/sidebars.js +9 -4
- package/reference/resource-management-di/scope.md +0 -1423
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: compile-time-resource-safety-with-scope
|
|
3
|
+
title: "Compile-Time Resource Safety with Scope"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Welcome to ZIO Blocks Scope—a library that makes resource management safe, composable, and verifiable at compile time. If you've ever struggled with `try/finally` chains, wondered when to close a database connection, or worried about resources outliving their owners, this tutorial is for you. You don't need any prior experience with Scope or advanced type system concepts to follow along.
|
|
7
|
+
|
|
8
|
+
## Learning Objectives
|
|
9
|
+
|
|
10
|
+
By the end of this tutorial, you will be able to:
|
|
11
|
+
|
|
12
|
+
- **Allocate resources safely** and trust the compiler to prevent use-after-close bugs
|
|
13
|
+
- **Understand the `$[A]` type and `scoped { }` block** — the two core mechanisms that make resource safety compile-time verifiable
|
|
14
|
+
- **Manage complex resource lifetimes** including cleanup with `defer`, nested scopes with `lower`, and reference counting with `Resource.shared`
|
|
15
|
+
- **Wire applications** using `Wire` and `Resource.from` for compile-time verified dependency injection
|
|
16
|
+
|
|
17
|
+
We'll learn through step-by-step examples that build from simple (allocating a single resource) to advanced patterns (nested scopes, shared resources, and dependency injection). Each section builds on the previous one, so we recommend reading from top to bottom.
|
|
18
|
+
|
|
19
|
+
## 1. The Problem: Why Resource Management Is Hard
|
|
20
|
+
|
|
21
|
+
Managing resources safely is deceptively difficult. When you open a database connection, read a file, or create a network socket, you must eventually close it—but only once, and only after you're done using it. Forget to close it, and you leak a resource. Close it too early, and you get a crash. Close it twice, and you get an error.
|
|
22
|
+
|
|
23
|
+
Nested resources make this worse. If you're reading a config file and then opening a database connection, you need nested `try/finally` blocks. If the inner resource throws an exception, the outer one may not close. Callbacks and closures can capture resources that outlive their intended scope, creating subtle use-after-free bugs.
|
|
24
|
+
|
|
25
|
+
Consider a typical `try/finally` pattern in Scala:
|
|
26
|
+
|
|
27
|
+
```scala
|
|
28
|
+
trait Connection extends AutoCloseable {
|
|
29
|
+
def createStatement(): Statement
|
|
30
|
+
}
|
|
31
|
+
trait Statement extends AutoCloseable {
|
|
32
|
+
def executeQuery(sql: String): ResultSet
|
|
33
|
+
}
|
|
34
|
+
trait ResultSet
|
|
35
|
+
def openConnection(): Connection = ???
|
|
36
|
+
def process(result: ResultSet): Unit = ???
|
|
37
|
+
def handleError(e: Exception): Unit = ???
|
|
38
|
+
val sql = ""
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
val connection = openConnection()
|
|
42
|
+
try {
|
|
43
|
+
val statement = connection.createStatement()
|
|
44
|
+
try {
|
|
45
|
+
val result = statement.executeQuery(sql)
|
|
46
|
+
process(result)
|
|
47
|
+
} finally {
|
|
48
|
+
statement.close()
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
connection.close()
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
case e: Exception => handleError(e)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This is hard to read, easy to get wrong, and doesn't compose—every additional resource adds another level of nesting. If an exception happens during cleanup, subsequent finalizers may not run. And if you pass a resource to another function, there's no compile-time guarantee that function won't use it after your scope ends.
|
|
59
|
+
|
|
60
|
+
Scope eliminates these problems by making resource ownership explicit and enforcing it at compile time.
|
|
61
|
+
|
|
62
|
+
## 2. Your First Scope
|
|
63
|
+
|
|
64
|
+
Let's start with the simplest possible example: allocating a single resource, using it, and letting it close automatically.
|
|
65
|
+
|
|
66
|
+
Scope builds on the concept of `Scope.global`—the root scope that outlives your entire program. You enter a scoped region using `scoped { }`, and inside that block, you can allocate resources. When the block exits, all resources close in reverse order (last allocated, first closed).
|
|
67
|
+
|
|
68
|
+
Here's a database connection that prints messages when opening and closing:
|
|
69
|
+
|
|
70
|
+
```scala
|
|
71
|
+
import zio.blocks.scope._
|
|
72
|
+
|
|
73
|
+
class Database extends AutoCloseable {
|
|
74
|
+
def connect(): Unit = println("Database: connecting")
|
|
75
|
+
def query(sql: String): String = s"Results of: $sql"
|
|
76
|
+
override def close(): Unit = println("Database: closing")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
Scope.global.scoped { scope =>
|
|
80
|
+
import scope._
|
|
81
|
+
val db: $[Database] = allocate(Resource {
|
|
82
|
+
val database = new Database()
|
|
83
|
+
database.connect()
|
|
84
|
+
database
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
$(db) { database =>
|
|
88
|
+
val result = database.query("SELECT * FROM users")
|
|
89
|
+
println(s"Query result: $result")
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Let's break down what happens:
|
|
95
|
+
|
|
96
|
+
- `Scope.global.scoped { scope => ... }` — Creates a scoped region. When the block exits, all allocated resources are closed.
|
|
97
|
+
- `import scope._` — Imports scope operations: `$`, `allocate`, and `defer`.
|
|
98
|
+
- `allocate(Resource { ... })` — Allocates a resource. Since `Database` extends `AutoCloseable`, its `close()` method is automatically registered as a finalizer.
|
|
99
|
+
- `$[Database]` — A scoped value of type `Database`. It can only be used within the scope where it was allocated.
|
|
100
|
+
- `$(db) { database => ... }` — Unwraps the scoped value and passes it to the block. This is the only way to access a resource.
|
|
101
|
+
|
|
102
|
+
When the scope exits, `database.close()` runs automatically, printing `"Database: closing"`.
|
|
103
|
+
|
|
104
|
+
With multiple resources, finalizers run in LIFO order (last allocated, first closed):
|
|
105
|
+
|
|
106
|
+
```scala
|
|
107
|
+
import zio.blocks.scope._
|
|
108
|
+
|
|
109
|
+
class Connection extends AutoCloseable {
|
|
110
|
+
def name: String = this.getClass.getSimpleName
|
|
111
|
+
override def close(): Unit = println(s"$name: closing")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
Scope.global.scoped { scope =>
|
|
115
|
+
import scope._
|
|
116
|
+
|
|
117
|
+
val conn1 = allocate(Resource(new Connection() { override def name = "Connection-1" }))
|
|
118
|
+
val conn2 = allocate(Resource(new Connection() { override def name = "Connection-2" }))
|
|
119
|
+
val conn3 = allocate(Resource(new Connection() { override def name = "Connection-3" }))
|
|
120
|
+
|
|
121
|
+
println("All connections allocated")
|
|
122
|
+
println("Exiting scope - connections will close in reverse order (3, 2, 1)")
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
:::note
|
|
127
|
+
The `$[A]` type is central to Scope's compile-time safety: each scope instance has a unique `$` type that cannot be mixed with other scopes. Resources allocated in one scope literally cannot be used in another, even at the type level. This prevents entire classes of resource-lifetime bugs at compile time.
|
|
128
|
+
:::
|
|
129
|
+
|
|
130
|
+
## 3. The `$[A]` Type and the `$` Operator
|
|
131
|
+
|
|
132
|
+
To understand Scope, you need to understand `$[A]`. It is a marker type that says "this value is owned by a specific scope." At runtime, `$[A]` is erased to `A` (zero overhead), but at compile time, it enforces a fundamental rule: **you can only use a resource via the operator defined by the scope it belongs to**.
|
|
133
|
+
|
|
134
|
+
Every scope instance has its own unique `$` type. Two different scopes have incompatible `$` types, so you cannot accidentally mix them at the type level. For example, if you allocate a resource in an outer scope and try to use it directly in an inner scope without `lower`, you get a compile error because the types are incompatible.
|
|
135
|
+
|
|
136
|
+
To use a parent-scoped resource in a child scope safely, you must use the `lower` operator, which we'll cover in Section 7.
|
|
137
|
+
|
|
138
|
+
To use a resource, apply the `$(value)` operator (it's a macro) with a single-argument block. The parameter must be used as the receiver of all operations:
|
|
139
|
+
|
|
140
|
+
```scala
|
|
141
|
+
import zio.blocks.scope._
|
|
142
|
+
|
|
143
|
+
class Logger extends AutoCloseable {
|
|
144
|
+
def log(msg: String): Unit = println(msg)
|
|
145
|
+
override def close(): Unit = ()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Scope.global.scoped { scope =>
|
|
149
|
+
import scope._
|
|
150
|
+
val logger = allocate(Resource(new Logger()))
|
|
151
|
+
|
|
152
|
+
// Correct: parameter used as receiver
|
|
153
|
+
$(logger) { log =>
|
|
154
|
+
log.log("Message 1")
|
|
155
|
+
log.log("Message 2")
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The following patterns will not compile:
|
|
161
|
+
- `$(logger) { log => logger.log("X") }` — cannot use `logger` outside the `$()` operator
|
|
162
|
+
- `$(logger) { log => val x = log; x }` — result is `$[Logger]`, which is not `Unscoped`
|
|
163
|
+
- `$(logger) { log => (log, "data") }` — tuples containing `$[Logger]` are not `Unscoped`
|
|
164
|
+
|
|
165
|
+
The `$` operator automatically unwraps the result if it is an `Unscoped[B]` type. We'll cover `Unscoped` in detail in Section 5, but for now, know that primitives like `Int`, `String`, and `Unit` are always `Unscoped`:
|
|
166
|
+
|
|
167
|
+
```scala
|
|
168
|
+
import zio.blocks.scope._
|
|
169
|
+
|
|
170
|
+
class Calculator extends AutoCloseable {
|
|
171
|
+
def add(a: Int, b: Int): Int = a + b
|
|
172
|
+
override def close(): Unit = ()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
Scope.global.scoped { scope =>
|
|
176
|
+
import scope._
|
|
177
|
+
val calc = allocate(Resource(new Calculator()))
|
|
178
|
+
|
|
179
|
+
// Result is Int, which is Unscoped, so it unwraps automatically
|
|
180
|
+
val sum = $(calc)(_.add(3, 4))
|
|
181
|
+
assert(sum == 7)
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
:::note
|
|
186
|
+
The `$` operator is not a regular method—it's a compile-time macro that inspects what you do with its parameter. This macro enforcement is what makes the rule "parameter must be receiver" checkable at compile time, not at runtime.
|
|
187
|
+
:::
|
|
188
|
+
|
|
189
|
+
## 4. Resources: Describing Acquisition and Cleanup
|
|
190
|
+
|
|
191
|
+
A `Resource[A]` is a lazy description of how to acquire a value and register any cleanup needed when the scope closes. Creating a resource does not acquire it—that only happens when you pass it to `scope.allocate()`.
|
|
192
|
+
|
|
193
|
+
There are several ways to construct a `Resource`:
|
|
194
|
+
|
|
195
|
+
**`Resource(value: => A)`** — The simplest form. Wraps a by-name value. If the value is `AutoCloseable`, its `close()` method is automatically registered:
|
|
196
|
+
|
|
197
|
+
```scala
|
|
198
|
+
import zio.blocks.scope._
|
|
199
|
+
|
|
200
|
+
class Connection extends AutoCloseable {
|
|
201
|
+
override def close(): Unit = println("Connection closed")
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
Scope.global.scoped { scope =>
|
|
205
|
+
import scope._
|
|
206
|
+
|
|
207
|
+
// By-name: creates connection on demand, closes automatically
|
|
208
|
+
val conn = allocate(Resource(new Connection()))
|
|
209
|
+
|
|
210
|
+
$(conn) { c =>
|
|
211
|
+
println("Using connection")
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**`Resource.acquireRelease(acquire)(release)`** — Explicit lifecycle control. Useful when cleanup is not a simple method call:
|
|
217
|
+
|
|
218
|
+
```scala
|
|
219
|
+
import zio.blocks.scope._
|
|
220
|
+
|
|
221
|
+
case class Connection(id: Int) {
|
|
222
|
+
def query(sql: String): String = s"[$id] $sql"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
Scope.global.scoped { scope =>
|
|
226
|
+
import scope._
|
|
227
|
+
|
|
228
|
+
// Explicit acquire and release
|
|
229
|
+
val conn = allocate(Resource.acquireRelease {
|
|
230
|
+
println("Opening connection...")
|
|
231
|
+
Connection(42)
|
|
232
|
+
} { c =>
|
|
233
|
+
println(s"Closing connection $c")
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
$(conn) { c =>
|
|
237
|
+
println(c.query("SELECT 1"))
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**`Resource.fromAutoCloseable(thunk)`** — Explicit wrapper for `AutoCloseable` subtypes. Type-safe and clear:
|
|
243
|
+
|
|
244
|
+
```scala
|
|
245
|
+
import zio.blocks.scope._
|
|
246
|
+
import java.io._
|
|
247
|
+
|
|
248
|
+
Scope.global.scoped { scope =>
|
|
249
|
+
import scope._
|
|
250
|
+
|
|
251
|
+
val file = allocate(Resource.fromAutoCloseable(new FileInputStream("/etc/hostname")))
|
|
252
|
+
|
|
253
|
+
$(file) { f =>
|
|
254
|
+
val bytes = Array.ofDim[Byte](100)
|
|
255
|
+
val n = f.read(bytes)
|
|
256
|
+
println(s"Read $n bytes")
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Resources compose: you can transform them with `map`, combine them with `zip`, or sequence them with `flatMap`:
|
|
262
|
+
|
|
263
|
+
```scala
|
|
264
|
+
import zio.blocks.scope._
|
|
265
|
+
|
|
266
|
+
case class Database(url: String) extends AutoCloseable {
|
|
267
|
+
def getConnection(name: String): Connection =
|
|
268
|
+
Connection(s"$url/$name")
|
|
269
|
+
override def close(): Unit = println(s"Database closed: $url")
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case class Connection(name: String) extends AutoCloseable {
|
|
273
|
+
def query(sql: String): String = s"[$name] $sql"
|
|
274
|
+
override def close(): Unit = println(s"Connection closed: $name")
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
Scope.global.scoped { scope =>
|
|
278
|
+
import scope._
|
|
279
|
+
|
|
280
|
+
val dbResource = Resource(new Database("localhost:5432"))
|
|
281
|
+
|
|
282
|
+
// flatMap sequences: database opens first, then connection opens from it
|
|
283
|
+
val connResource = dbResource.flatMap { db =>
|
|
284
|
+
Resource(db.getConnection("myapp"))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
val conn = allocate(connResource)
|
|
288
|
+
|
|
289
|
+
$(conn) { c =>
|
|
290
|
+
println(c.query("SELECT 1"))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
When you use `flatMap` to open a connection from an already-open database, the database stays open until the scope closes, ensuring the connection is always valid.
|
|
296
|
+
|
|
297
|
+
## 5. Returning Values: The `Unscoped[A]` Typeclass
|
|
298
|
+
|
|
299
|
+
When you exit a `scoped { }` block, the scope closes and all resources are finalized. But what can you return from a `scoped` block? A scoped value `$[A]` cannot escape—it would be used after the scope closes. That's where `Unscoped[A]` comes in.
|
|
300
|
+
|
|
301
|
+
`Unscoped[A]` is a typeclass that marks types as safe to return from a scoped block. It means "this type contains no scope-bound resources; it is pure data." The type system only allows returning a value if it has an `Unscoped` instance:
|
|
302
|
+
|
|
303
|
+
```scala
|
|
304
|
+
import zio.blocks.scope._
|
|
305
|
+
|
|
306
|
+
case class Config(host: String, port: Int)
|
|
307
|
+
|
|
308
|
+
Scope.global.scoped { scope =>
|
|
309
|
+
import scope._
|
|
310
|
+
|
|
311
|
+
// Config is a case class—it has Unscoped by default
|
|
312
|
+
Config("localhost", 5432)
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Built-in `Unscoped` instances include primitives (`Int`, `String`, `Boolean`), collections (`List[A]`, `Map[K, V]`), and common library types (`UUID`, `java.time.LocalDate`). If you define a case class with no resource fields, it automatically gets an `Unscoped` instance:
|
|
317
|
+
|
|
318
|
+
```scala
|
|
319
|
+
import zio.blocks.scope._
|
|
320
|
+
|
|
321
|
+
case class Result(count: Int, message: String)
|
|
322
|
+
case class ServerConfig(host: String, port: Int, timeout: Long)
|
|
323
|
+
|
|
324
|
+
Scope.global.scoped { scope =>
|
|
325
|
+
import scope._
|
|
326
|
+
|
|
327
|
+
// Both can be returned—they have implicit Unscoped instances
|
|
328
|
+
Result(42, "success") -> ServerConfig("0.0.0.0", 8080, 30000)
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
If you define a custom class and want to return it from `scoped`, you need to either derive or provide an `Unscoped` instance. In Scala 3, case classes support automatic derivation via `derives`:
|
|
333
|
+
|
|
334
|
+
```scala
|
|
335
|
+
import zio.blocks.scope._
|
|
336
|
+
|
|
337
|
+
case class CustomData(x: Int, y: String) derives Unscoped
|
|
338
|
+
|
|
339
|
+
Scope.global.scoped { scope =>
|
|
340
|
+
import scope._
|
|
341
|
+
CustomData(10, "hello")
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
Alternatively, provide an instance explicitly using a `given`:
|
|
346
|
+
|
|
347
|
+
```scala
|
|
348
|
+
import zio.blocks.scope._
|
|
349
|
+
|
|
350
|
+
case class CustomData(x: Int, y: String)
|
|
351
|
+
|
|
352
|
+
given Unscoped[CustomData] = Unscoped.derived
|
|
353
|
+
|
|
354
|
+
Scope.global.scoped { scope =>
|
|
355
|
+
import scope._
|
|
356
|
+
CustomData(10, "hello")
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
If you try to return a scoped value without an `Unscoped` instance, you get a compile error:
|
|
361
|
+
|
|
362
|
+
```scala
|
|
363
|
+
import zio.blocks.scope._
|
|
364
|
+
|
|
365
|
+
class Connection extends AutoCloseable {
|
|
366
|
+
override def close(): Unit = ()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// This does not compile because Connection has no Unscoped instance:
|
|
370
|
+
// val conn = Scope.global.scoped { scope =>
|
|
371
|
+
// import scope._
|
|
372
|
+
// allocate(Resource(new Connection()))
|
|
373
|
+
// }
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
This compile-time barrier prevents entire classes of resource-lifetime bugs—you cannot accidentally return a resource reference from a scoped block.
|
|
377
|
+
|
|
378
|
+
## 6. Finalizers and Error Handling
|
|
379
|
+
|
|
380
|
+
Sometimes you need to register cleanup that is not a simple resource `close()`. The `defer` operator lets you register arbitrary cleanup actions:
|
|
381
|
+
|
|
382
|
+
```scala
|
|
383
|
+
import zio.blocks.scope._
|
|
384
|
+
|
|
385
|
+
case class Transaction(id: Int) {
|
|
386
|
+
def begin(): Unit = println(s"Transaction $id: begin")
|
|
387
|
+
def commit(): Unit = println(s"Transaction $id: commit")
|
|
388
|
+
def rollback(): Unit = println(s"Transaction $id: rollback")
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
Scope.global.scoped { scope =>
|
|
392
|
+
import scope._
|
|
393
|
+
|
|
394
|
+
val txn = Transaction(1)
|
|
395
|
+
txn.begin()
|
|
396
|
+
|
|
397
|
+
// Register rollback as cleanup
|
|
398
|
+
scope.defer {
|
|
399
|
+
txn.rollback()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// If we commit, cancel the rollback
|
|
403
|
+
txn.commit()
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`scope.defer()` returns a `DeferHandle` that lets you cancel the finalizer before it runs:
|
|
408
|
+
|
|
409
|
+
```scala
|
|
410
|
+
import zio.blocks.scope._
|
|
411
|
+
|
|
412
|
+
case class Transaction(id: Int) {
|
|
413
|
+
def begin(): Unit = println(s"Transaction $id: begin")
|
|
414
|
+
def commit(): Unit = println(s"Transaction $id: commit")
|
|
415
|
+
def rollback(): Unit = println(s"Transaction $id: rollback")
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
Scope.global.scoped { scope =>
|
|
419
|
+
import scope._
|
|
420
|
+
|
|
421
|
+
val txn = Transaction(1)
|
|
422
|
+
txn.begin()
|
|
423
|
+
|
|
424
|
+
// Register rollback, but keep the handle so we can cancel it
|
|
425
|
+
val rollbackHandle = scope.defer {
|
|
426
|
+
txn.rollback()
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// On success, cancel the rollback finalizer
|
|
430
|
+
txn.commit()
|
|
431
|
+
rollbackHandle.cancel()
|
|
432
|
+
|
|
433
|
+
println("Scope exiting - rollback will NOT run because we cancelled it")
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Finalizers run in **LIFO order** (last registered, first executed) and are guaranteed to run even if the scoped block throws an exception. If multiple finalizers throw, they are collected:
|
|
438
|
+
|
|
439
|
+
```scala
|
|
440
|
+
import zio.blocks.scope._
|
|
441
|
+
|
|
442
|
+
Scope.global.scoped { scope =>
|
|
443
|
+
import scope._
|
|
444
|
+
|
|
445
|
+
scope.defer { println("Finalizer 1") }
|
|
446
|
+
scope.defer { println("Finalizer 2") }
|
|
447
|
+
scope.defer { println("Finalizer 3") }
|
|
448
|
+
|
|
449
|
+
println("Block executing")
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
When this code runs, the finalizers execute in reverse order of registration:
|
|
454
|
+
|
|
455
|
+
```
|
|
456
|
+
Block executing
|
|
457
|
+
Finalizer 3
|
|
458
|
+
Finalizer 2
|
|
459
|
+
Finalizer 1
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
:::note
|
|
463
|
+
The `DeferHandle.cancel()` operation is O(1)—it marks the finalizer as cancelled without traversing the entire registry. This makes it safe to use in performance-sensitive code, like transaction commits in tight loops.
|
|
464
|
+
:::
|
|
465
|
+
|
|
466
|
+
## 7. Nested Scopes and `lower`
|
|
467
|
+
|
|
468
|
+
Scopes form a tree: each scope can create child scopes via `scope.scoped { }`. Children are guaranteed to close before their parent, which is the foundation of hierarchical resource management.
|
|
469
|
+
|
|
470
|
+
But child scopes have a different `$[A]` type than their parent, so a parent-scoped value cannot be directly used in a child. That's where `lower` comes in. `Scope#lower` re-tags a parent-scoped value into a child scope, which is safe because the parent always outlives the child:
|
|
471
|
+
|
|
472
|
+
```scala
|
|
473
|
+
import zio.blocks.scope._
|
|
474
|
+
|
|
475
|
+
class Database(name: String) extends AutoCloseable {
|
|
476
|
+
def query(sql: String): String = s"[$name] $sql"
|
|
477
|
+
override def close(): Unit = println(s"Database [$name] closed")
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
class Connection(db: String, id: Int) extends AutoCloseable {
|
|
481
|
+
def query(sql: String): String = s"[$db/$id] $sql"
|
|
482
|
+
override def close(): Unit = println(s"Connection [$db/$id] closed")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
Scope.global.scoped { parentScope =>
|
|
486
|
+
import parentScope._
|
|
487
|
+
|
|
488
|
+
// Open database in parent scope
|
|
489
|
+
val db = allocate(Resource(new Database("maindb")))
|
|
490
|
+
|
|
491
|
+
// Use database in parent scope
|
|
492
|
+
$(db) { database =>
|
|
493
|
+
println(s"Parent: ${database.query("SELECT 1")}")
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Create a child scope (e.g., for a request)
|
|
497
|
+
parentScope.scoped { childScope =>
|
|
498
|
+
import childScope._
|
|
499
|
+
|
|
500
|
+
// Lower the parent-scoped database into the child scope
|
|
501
|
+
val dbInChild = childScope.lower(db)
|
|
502
|
+
|
|
503
|
+
// Now we can use the database in the child scope
|
|
504
|
+
val conn = allocate(Resource(new Connection("maindb", 1)))
|
|
505
|
+
|
|
506
|
+
$(dbInChild) { database =>
|
|
507
|
+
$(conn) { connection =>
|
|
508
|
+
println(s"Child: ${connection.query("SELECT 2")}")
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
println("Child scope exiting - connection closes first")
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
println("Parent scope exiting - database closes after children")
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
When the child scope exits, all resources allocated in it close first. Then the parent scope's finalizers run. This ensures that if a child holds a reference to a parent's resource, that resource is not closed until all children have finished.
|
|
520
|
+
|
|
521
|
+
## 8. Explicit Lifetime Management with `open()`
|
|
522
|
+
|
|
523
|
+
The `scoped { }` syntax ties resource lifetime to a lexical block. But sometimes you need explicit, decoupled lifetime management—for example, a request handler that opens a connection when processing begins and closes it when processing ends, independent of any fixed scope nesting.
|
|
524
|
+
|
|
525
|
+
Child scopes created via `scoped { }` are owned by the thread that creates them and must close within the creating thread. But `Scope.global.open()` creates an unowned scope that can be closed from any thread. This is useful for bridging structured scope-based resource management with callbacks or cross-thread communication:
|
|
526
|
+
|
|
527
|
+
```scala
|
|
528
|
+
import zio.blocks.scope._
|
|
529
|
+
|
|
530
|
+
class ConnectionPool extends AutoCloseable {
|
|
531
|
+
def acquire(): String = "conn-001"
|
|
532
|
+
override def close(): Unit = println("Pool closed")
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Simulate a request handler in an async framework
|
|
536
|
+
case class RequestContext(id: Int) {
|
|
537
|
+
var connection: String = ""
|
|
538
|
+
|
|
539
|
+
def setConnection(c: String): Unit = {
|
|
540
|
+
connection = c
|
|
541
|
+
println(s"Processing request ${id}, connection: $connection")
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Using open() to manage lifetime explicitly, decoupled from lexical scope
|
|
546
|
+
val request = RequestContext(1)
|
|
547
|
+
|
|
548
|
+
Scope.global.scoped { scope =>
|
|
549
|
+
import scope._
|
|
550
|
+
|
|
551
|
+
// open() creates an unowned scope that can be closed explicitly
|
|
552
|
+
$(open()) { handle =>
|
|
553
|
+
val requestScope = handle.scope
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
// Use the scope to allocate and manage the resource
|
|
557
|
+
requestScope.scoped { innerScope =>
|
|
558
|
+
import innerScope._
|
|
559
|
+
|
|
560
|
+
val pool = allocate(Resource(new ConnectionPool()))
|
|
561
|
+
|
|
562
|
+
$(pool) { p =>
|
|
563
|
+
request.setConnection(p.acquire())
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} finally {
|
|
567
|
+
// Close the open scope explicitly and handle any finalizer failures
|
|
568
|
+
handle.close().orThrow()
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
The standard pattern for managing resource lifetimes in your application is to use `scoped { }` with careful nesting. The `open()` method is reserved for low-level integration points (like application startup/shutdown boundaries) and is not typically needed in application code.
|
|
575
|
+
|
|
576
|
+
:::note
|
|
577
|
+
Thread ownership is enforced for child scopes created with `scoped { }` but not for unowned scopes from `open()`. This difference allows Scope to prevent thread-related bugs in structured code while still supporting integration with callback-driven or asynchronous frameworks that require explicit lifetime management.
|
|
578
|
+
:::
|
|
579
|
+
|
|
580
|
+
## 9. Shared Resources and Reference Counting
|
|
581
|
+
|
|
582
|
+
When multiple parts of your application need the same heavyweight resource (like a database connection pool), you want to create it once and destroy it only when the last user is done. `Resource.shared` provides reference-counted sharing:
|
|
583
|
+
|
|
584
|
+
```scala
|
|
585
|
+
import zio.blocks.scope._
|
|
586
|
+
|
|
587
|
+
class ConnectionPool(id: Int) extends AutoCloseable {
|
|
588
|
+
def getConnection(): String = s"conn-from-pool-$id"
|
|
589
|
+
override def close(): Unit = println(s"Pool $id closed")
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
case class UserService(poolId: String)
|
|
593
|
+
case class OrderService(poolId: String)
|
|
594
|
+
|
|
595
|
+
Scope.global.scoped { scope =>
|
|
596
|
+
import scope._
|
|
597
|
+
|
|
598
|
+
// Create a shared resource: only one pool instance, reference-counted
|
|
599
|
+
val sharedPool = Resource.shared[ConnectionPool] { _ =>
|
|
600
|
+
println("Creating shared pool...")
|
|
601
|
+
new ConnectionPool(42)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Both services allocate from the same shared resource
|
|
605
|
+
// The pool is created once, destroyed after both services release it
|
|
606
|
+
val pool1 = allocate(sharedPool)
|
|
607
|
+
val pool2 = allocate(sharedPool)
|
|
608
|
+
|
|
609
|
+
$(pool1) { p1 =>
|
|
610
|
+
$(pool2) { p2 =>
|
|
611
|
+
// Both p1 and p2 point to the same pool instance
|
|
612
|
+
println(s"Service 1 got: ${p1.getConnection()}")
|
|
613
|
+
println(s"Service 2 got: ${p2.getConnection()}")
|
|
614
|
+
println("Both services are using the same shared pool instance")
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
println("Scope exiting - shared pool closed (once)")
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
`Resource.shared` is memoized: the first `allocate` creates the pool, and subsequent `allocate` calls get the same instance. The finalizer runs only after all allocations have released their reference (implicitly when the scope closes).
|
|
623
|
+
|
|
624
|
+
This pattern is essential for applications with a shared database connection pool, cache, or logging infrastructure.
|
|
625
|
+
|
|
626
|
+
## 10. Dependency Injection with Wire
|
|
627
|
+
|
|
628
|
+
Applications often have many services with interdependencies. Manual wiring is error-prone: forget a dependency, pass the wrong type, create a cycle, or accidentally duplicate an instance where sharing was intended.
|
|
629
|
+
|
|
630
|
+
`Wire` and `Resource.from` provide compile-time dependency injection. Wires are builders that describe how to construct instances, and `Resource.from` resolves the entire dependency graph:
|
|
631
|
+
|
|
632
|
+
```scala
|
|
633
|
+
import zio.blocks.scope._
|
|
634
|
+
|
|
635
|
+
case class DbConfig(url: String)
|
|
636
|
+
case class Database(config: DbConfig) extends AutoCloseable {
|
|
637
|
+
override def close(): Unit = println("Database closed")
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
case class CacheService(db: Database)
|
|
641
|
+
case class AuthService(db: Database)
|
|
642
|
+
case class AppService(cache: CacheService, auth: AuthService)
|
|
643
|
+
|
|
644
|
+
Scope.global.scoped { scope =>
|
|
645
|
+
import scope._
|
|
646
|
+
|
|
647
|
+
// Wire.shared means all dependents get the same instance
|
|
648
|
+
val configWire = Wire(DbConfig("localhost"))
|
|
649
|
+
val dbWire = Wire.shared[Database]
|
|
650
|
+
val cacheWire = Wire.shared[CacheService]
|
|
651
|
+
val authWire = Wire.shared[AuthService]
|
|
652
|
+
val appWire = Wire.shared[AppService]
|
|
653
|
+
|
|
654
|
+
// Resource.from resolves all wires and returns the root app service
|
|
655
|
+
val app = allocate(Resource.from[AppService](
|
|
656
|
+
configWire, dbWire, cacheWire, authWire, appWire
|
|
657
|
+
))
|
|
658
|
+
|
|
659
|
+
$(app) { a =>
|
|
660
|
+
println("App service created: " + a.toString())
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
The compiler verifies that:
|
|
666
|
+
- Every dependency can be satisfied.
|
|
667
|
+
- No unsatisfiable circular dependencies exist.
|
|
668
|
+
- Types match correctly.
|
|
669
|
+
|
|
670
|
+
If you violate any of these rules, you get a clear compile error before runtime.
|
|
671
|
+
|
|
672
|
+
## 11. Thread Ownership
|
|
673
|
+
|
|
674
|
+
On the JVM, Scope enforces a structured concurrency guarantee: each `Scope.Child` (any scope created with `scoped { }` or as a child of another scope) is owned by the thread that created it. This prevents a subtle class of bugs where a scope reference escapes to another thread and resources are used or closed on the wrong thread.
|
|
675
|
+
|
|
676
|
+
You can check ownership with `Scope#isOwner`:
|
|
677
|
+
|
|
678
|
+
```scala
|
|
679
|
+
import zio.blocks.scope._
|
|
680
|
+
|
|
681
|
+
Scope.global.scoped { scope =>
|
|
682
|
+
println(s"Global scope owned by current thread: ${scope.isOwner}")
|
|
683
|
+
|
|
684
|
+
scope.scoped { childScope =>
|
|
685
|
+
println(s"Child scope owned by current thread: ${childScope.isOwner}")
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
If you try to use a child scope from a different thread, operations like `allocate`, `defer`, and `$(value)()` throw an `IllegalStateException`:
|
|
691
|
+
|
|
692
|
+
```scala
|
|
693
|
+
import zio.blocks.scope._
|
|
694
|
+
|
|
695
|
+
class Database extends AutoCloseable {
|
|
696
|
+
override def close(): Unit = ()
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
Scope.global.scoped { scope =>
|
|
700
|
+
import scope._
|
|
701
|
+
|
|
702
|
+
val db = allocate(Resource(new Database()))
|
|
703
|
+
|
|
704
|
+
// Attempting to use the scope from another thread will throw:
|
|
705
|
+
// val thread = new Thread(() => {
|
|
706
|
+
// $(db) { _ => } // throws IllegalStateException: ownership violation
|
|
707
|
+
// })
|
|
708
|
+
// thread.start()
|
|
709
|
+
}
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
To pass a resource to another thread safely, use `Scope.global.open()` to create an unowned scope, or redesign to keep all operations on the creating thread.
|
|
713
|
+
|
|
714
|
+
For platforms like Scala.js (single-threaded), thread ownership checks are disabled—ownership is always considered valid.
|
|
715
|
+
|
|
716
|
+
:::note
|
|
717
|
+
Thread ownership enforcement is not about thread safety in the traditional sense—it's about structured concurrency. It prevents subtle bugs where a scope escapes to another thread and resources are closed on a different thread than they were allocated.
|
|
718
|
+
:::
|
|
719
|
+
|
|
720
|
+
## 12. Common Errors and Troubleshooting
|
|
721
|
+
|
|
722
|
+
This section lists the most common runtime and compile errors, explains what caused them, and how to fix them.
|
|
723
|
+
|
|
724
|
+
### Runtime Errors
|
|
725
|
+
|
|
726
|
+
**`IllegalStateException: Scope is closed`** when calling `allocate`, `defer`, `$`, or `open` on a closed scope:
|
|
727
|
+
|
|
728
|
+
```scala
|
|
729
|
+
import zio.blocks.scope._
|
|
730
|
+
import zio.blocks.scope.Resource
|
|
731
|
+
|
|
732
|
+
class Database extends AutoCloseable {
|
|
733
|
+
override def close(): Unit = ()
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
var db: Option[Database] = None // Wrong: trying to escape scoped value
|
|
737
|
+
|
|
738
|
+
Scope.global.scoped { scope =>
|
|
739
|
+
import scope._
|
|
740
|
+
// db = Some(allocate(Resource.fromAutoCloseable(new Database()))) // Error: can't assign scope.$[Database] to Option[Database]
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// scope is now closed; this throws IllegalStateException:
|
|
744
|
+
// db.foreach(_.close())
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**Fix:** Ensure all resource usage happens before the scoped block exits. If you need to return a resource reference, return only the underlying value (wrapped in an `Unscoped` type).
|
|
748
|
+
|
|
749
|
+
**`IllegalStateException: Thread ownership violation`** when calling operations on a child scope from a different thread:
|
|
750
|
+
|
|
751
|
+
```scala
|
|
752
|
+
import zio.blocks.scope._
|
|
753
|
+
import scala.concurrent.Future
|
|
754
|
+
import scala.concurrent.ExecutionContext.Implicits.global
|
|
755
|
+
|
|
756
|
+
var scope: Option[Scope] = None
|
|
757
|
+
|
|
758
|
+
Scope.global.scoped { s =>
|
|
759
|
+
scope = Some(s)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
Future {
|
|
763
|
+
// This throws: the scope was created on the main thread, not this one
|
|
764
|
+
// scope.foreach(_.scoped { _ => })
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
**Fix:** Use `Scope.global.open()` to create an unowned scope that can be shared across threads, or keep all operations on the creating thread.
|
|
769
|
+
|
|
770
|
+
### Compile Errors
|
|
771
|
+
|
|
772
|
+
**No `Unscoped` instance for type `T`** when trying to return a value from `scoped`:
|
|
773
|
+
|
|
774
|
+
```scala
|
|
775
|
+
import zio.blocks.scope._
|
|
776
|
+
|
|
777
|
+
class Connection extends AutoCloseable {
|
|
778
|
+
override def close(): Unit = ()
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// This does not compile:
|
|
782
|
+
// val conn = Scope.global.scoped { scope =>
|
|
783
|
+
// import scope._
|
|
784
|
+
// allocate(Resource(new Connection())) // ERROR: $[Connection] has no Unscoped
|
|
785
|
+
// }
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**Fix:** Only return types with an `Unscoped` instance (primitives, case classes, collections). If you need to return a resource reference, extract its underlying value first, or use `Wire` to manage the resource's lifetime.
|
|
789
|
+
|
|
790
|
+
**Cannot call method directly on `$[T]`**:
|
|
791
|
+
|
|
792
|
+
```scala
|
|
793
|
+
import zio.blocks.scope._
|
|
794
|
+
|
|
795
|
+
class Logger extends AutoCloseable {
|
|
796
|
+
def log(msg: String): Unit = println(msg)
|
|
797
|
+
override def close(): Unit = ()
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// This does not compile:
|
|
801
|
+
// Scope.global.scoped { scope =>
|
|
802
|
+
// import scope._
|
|
803
|
+
// val logger = allocate(Resource(new Logger()))
|
|
804
|
+
// logger.log("test") // ERROR: method log not visible on $[Logger]
|
|
805
|
+
// }
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
**Fix:** Use the `$` operator to unwrap: `$(logger)(_.log("test"))`.
|
|
809
|
+
|
|
810
|
+
**`Wire` cannot resolve dependency** when wiring fails due to missing constructor arguments:
|
|
811
|
+
|
|
812
|
+
```scala
|
|
813
|
+
import zio.blocks.scope._
|
|
814
|
+
|
|
815
|
+
case class Database(url: String) extends AutoCloseable {
|
|
816
|
+
override def close(): Unit = ()
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
// If Database constructor is not satisfied by wires, compilation fails:
|
|
820
|
+
// val dbWire: Wire[Database] = Wire.shared[Database] // ERROR: no Wire[String] for url
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
**Fix:** Provide a wire for every required dependency:
|
|
824
|
+
|
|
825
|
+
```scala
|
|
826
|
+
import zio.blocks.scope._
|
|
827
|
+
|
|
828
|
+
case class Database(url: String) extends AutoCloseable {
|
|
829
|
+
override def close(): Unit = ()
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Correct: provide the String
|
|
833
|
+
val urlWire = Wire("localhost")
|
|
834
|
+
val dbWire = Wire.shared[Database]
|
|
835
|
+
|
|
836
|
+
Scope.global.scoped { scope =>
|
|
837
|
+
import scope._
|
|
838
|
+
val db = allocate(Resource.from[Database](urlWire, dbWire))
|
|
839
|
+
$(db) { d => println("Connected to " + d.toString()) }
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
---
|
|
844
|
+
|
|
845
|
+
## Putting It Together
|
|
846
|
+
|
|
847
|
+
Now let's combine everything we've learned into a single, realistic example. This example demonstrates:
|
|
848
|
+
|
|
849
|
+
- Multiple allocated resources (database and logger)
|
|
850
|
+
- Wire-based dependency injection with shared and unique wires
|
|
851
|
+
- Automatic cleanup in reverse allocation order
|
|
852
|
+
- The complete interaction of all concepts
|
|
853
|
+
|
|
854
|
+
This example combines core concepts — allocation, cleanup, resource composition, and dependency injection:
|
|
855
|
+
|
|
856
|
+
```scala
|
|
857
|
+
import zio.blocks.scope._
|
|
858
|
+
|
|
859
|
+
class Database extends AutoCloseable {
|
|
860
|
+
def query(sql: String): String = s"Results: $sql"
|
|
861
|
+
override def close(): Unit = println("Closing database")
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
class Logger extends AutoCloseable {
|
|
865
|
+
def log(msg: String): Unit = println(s"[LOG] $msg")
|
|
866
|
+
override def close(): Unit = println("Closing logger")
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
class Application(database: Database, logger: Logger) extends AutoCloseable {
|
|
870
|
+
def run(): String = {
|
|
871
|
+
logger.log("Starting application")
|
|
872
|
+
val result = database.query("SELECT * FROM users")
|
|
873
|
+
logger.log(s"Query executed: $result")
|
|
874
|
+
result
|
|
875
|
+
}
|
|
876
|
+
override def close(): Unit = logger.log("Shutting down application")
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Create an app using Scope with wire-based dependency injection
|
|
880
|
+
Scope.global.scoped { scope =>
|
|
881
|
+
import scope._
|
|
882
|
+
|
|
883
|
+
// Wire.shared means all dependents receive the same Logger instance
|
|
884
|
+
// Wire.shared[Database] means all dependents receive the same Database
|
|
885
|
+
// Resource.from[Application] constructs Application by resolving dependencies
|
|
886
|
+
val app = allocate(
|
|
887
|
+
Resource.from[Application](
|
|
888
|
+
Wire.shared[Database],
|
|
889
|
+
Wire.shared[Logger]
|
|
890
|
+
)
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
// Use the application
|
|
894
|
+
$(app) { a =>
|
|
895
|
+
val result = a.run()
|
|
896
|
+
println(s"Result: $result")
|
|
897
|
+
|
|
898
|
+
// Register additional cleanup operations
|
|
899
|
+
defer {
|
|
900
|
+
println("Application cleanup complete")
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// All resources are closed in LIFO order: Application, Logger, then Database
|
|
905
|
+
()
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
println("Program finished")
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
This example demonstrates:
|
|
912
|
+
- **Wire-based DI**: `Wire.shared[T]` automatically derives how to construct `T` from its dependencies
|
|
913
|
+
- **Resource.from**: The macro analyzes the dependency graph and automatically allocates in correct order
|
|
914
|
+
- **Composition**: Multiple resources (Database, Logger, Application) with declared dependencies
|
|
915
|
+
- **Cleanup**: All resources close in reverse order when the scope exits (LIFO)
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
## What You've Learned
|
|
920
|
+
|
|
921
|
+
In this tutorial, you learned:
|
|
922
|
+
|
|
923
|
+
- What Scope is and why compile-time resource safety matters
|
|
924
|
+
- How to allocate, use, and manage resources with the `scoped { }` block
|
|
925
|
+
- The `$[A]` type and how it enforces resource ownership at the type level
|
|
926
|
+
- How to construct resources with `Resource` and its variants
|
|
927
|
+
- The `Unscoped[A]` typeclass and why returning resources is forbidden
|
|
928
|
+
- How to register cleanup with `defer` and manage finalizer order
|
|
929
|
+
- Nested scopes and the `lower` operator for hierarchical resource management
|
|
930
|
+
- Advanced patterns like `open()` for decoupled lifetime management, `Resource.shared` for reference counting, and `Wire` for dependency injection
|
|
931
|
+
- Common errors and how to fix them
|
|
932
|
+
|
|
933
|
+
You now have a solid foundation in Scope. The next step is to see how to apply these concepts in practice with realistic scenarios.
|
|
934
|
+
|
|
935
|
+
---
|
|
936
|
+
|
|
937
|
+
## Running the Examples
|
|
938
|
+
|
|
939
|
+
The code examples in this tutorial are embedded directly in the documentation and compile with mdoc. To run them locally:
|
|
940
|
+
|
|
941
|
+
**1. Clone the repository and navigate to the project:**
|
|
942
|
+
|
|
943
|
+
```bash
|
|
944
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
945
|
+
cd zio-blocks
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
**2. Compile and run the tutorial examples:**
|
|
949
|
+
|
|
950
|
+
All examples from the tutorial sections are compile-checked using mdoc. To verify they compile:
|
|
951
|
+
|
|
952
|
+
```bash
|
|
953
|
+
sbt "docs/mdoc --in docs/guides/compile-time-resource-safety-with-scope.md"
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
**3. Run standalone examples from the scope-examples module:**
|
|
957
|
+
|
|
958
|
+
The repository also includes additional companion examples in `scope-examples/`. For example:
|
|
959
|
+
|
|
960
|
+
```bash
|
|
961
|
+
sbt "scope-examples/runMain runDatabaseExample"
|
|
962
|
+
sbt "scope-examples/runMain runCachingExample"
|
|
963
|
+
sbt "scope-examples/runMain runThreadOwnershipExample"
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
To compile all examples:
|
|
967
|
+
|
|
968
|
+
```bash
|
|
969
|
+
sbt "scope-examples/compile"
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
---
|
|
973
|
+
|
|
974
|
+
## Where to Go Next
|
|
975
|
+
|
|
976
|
+
- **Ready to use this in practice?** Check out the how-to guides (coming soon) which walk through real-world examples.
|
|
977
|
+
- **Want to dive deeper into the API?** Read the [Scope Reference](../reference/resource-management/scope.md) for comprehensive API documentation.
|
|
978
|
+
- **Interested in related concepts?** Explore dependency injection with [Wire](../reference/resource-management/wire.md) or resource composition patterns.
|
|
979
|
+
|
|
980
|
+
---
|
|
981
|
+
|
|
982
|
+
## Summary
|
|
983
|
+
|
|
984
|
+
You now understand Scope's core concepts:
|
|
985
|
+
|
|
986
|
+
- **`$[A]`** — a type-level owner tag that prevents resources from escaping their scope.
|
|
987
|
+
- **`scoped { }`** — the syntax for entering and exiting a scope.
|
|
988
|
+
- **`allocate` and `defer`** — operations to register resources and cleanup.
|
|
989
|
+
- **`Resource`** — lazy descriptions of acquisition and cleanup.
|
|
990
|
+
- **`Unscoped`** — a compile-time guarantee that a type is safe to return from a scope.
|
|
991
|
+
- **Nesting and `lower`** — hierarchical resource management with compile-time parent-child relationships.
|
|
992
|
+
- **Shared resources** — reference counting for multiply-used resources.
|
|
993
|
+
- **`Wire` and dependency injection** — compile-time-verified wiring of complex applications.
|
|
994
|
+
- **Thread ownership** — JVM enforcement of structured concurrency.
|
|
995
|
+
|
|
996
|
+
For complete API documentation, see the [Scope Reference](../reference/resource-management/scope.md).
|
|
997
|
+
|