@zio.dev/zio-blocks 0.0.28 → 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.
Files changed (35) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +997 -0
  2. package/guides/query-dsl-extending.md +1 -1
  3. package/guides/query-dsl-fluent-builder.md +203 -203
  4. package/guides/query-dsl-reified-optics.md +1 -1
  5. package/guides/query-dsl-sql.md +1 -1
  6. package/guides/zio-schema-migration.md +6 -6
  7. package/index.md +68 -16
  8. package/package.json +1 -1
  9. package/path-interpolator.md +70 -9
  10. package/reference/allows.md +96 -0
  11. package/reference/codec.md +8 -8
  12. package/reference/combinators.md +345 -0
  13. package/reference/context.md +639 -67
  14. package/reference/docs.md +1 -1
  15. package/reference/dynamic-schema.md +39 -42
  16. package/reference/http-model.md +1716 -0
  17. package/reference/json-differ.md +320 -0
  18. package/reference/json-patch.md +1 -1
  19. package/reference/media-type.md +2 -2
  20. package/reference/resource-management/defer-handle.md +246 -0
  21. package/reference/resource-management/finalization.md +286 -0
  22. package/reference/resource-management/finalizer.md +167 -0
  23. package/reference/resource-management/index.md +44 -0
  24. package/reference/resource-management/resource.md +1607 -0
  25. package/reference/resource-management/scope.md +3026 -0
  26. package/reference/resource-management/unscoped.md +125 -0
  27. package/reference/resource-management/wire.md +878 -0
  28. package/reference/schema-evolution/as.md +4 -4
  29. package/reference/schema-evolution/into.md +2 -2
  30. package/reference/schema-expr.md +2 -2
  31. package/reference/streams.md +989 -0
  32. package/reference/type-class-derivation.md +31 -31
  33. package/ringbuffer.md +249 -0
  34. package/sidebars.js +19 -2
  35. package/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
+