@zio.dev/zio-blocks 0.0.28 → 0.0.29

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.
@@ -3,155 +3,727 @@ id: context
3
3
  title: "Context"
4
4
  ---
5
5
 
6
- `Context[+R]` is a type-indexed heterogeneous collection. It stores values of different types, indexed by their types, with compile-time type safety for lookups.
6
+ `Context[+R]` is a type-indexed heterogeneous collection that stores values of different types, indexed by their types, with compile-time type safety for lookups. It provides an immutable, cache-aware dependency container where the phantom type `R` (using intersection types) tracks which types are present.
7
+
8
+ The core type looks like this:
9
+
10
+ ```scala
11
+ // Signature (showing public API structure, not actual implementation)
12
+ final class Context[+R] {
13
+ def size: Int
14
+ def isEmpty: Boolean
15
+ def nonEmpty: Boolean
16
+ def get[A >: R](implicit ev: IsNominalType[A]): A
17
+ def getOption[A](implicit ev: IsNominalType[A]): Option[A]
18
+ def add[A](a: A)(implicit ev: IsNominalType[A]): Context[R & A]
19
+ def update[A >: R](f: A => A)(implicit ev: IsNominalType[A]): Context[R]
20
+ def ++[R1](that: Context[R1]): Context[R & R1]
21
+ def prune[A >: R](implicit ev: IsNominalIntersection[A]): Context[A]
22
+ override def toString: String
23
+ }
24
+ ```
25
+
26
+ Key properties: covariant (`+R`), immutable, cached for repeated lookups, supports only nominal types.
7
27
 
8
28
  ## Overview
9
29
 
30
+ Context serves as a type-safe registry for heterogeneous dependencies. Here's a quick example:
31
+
10
32
  ```scala
11
33
  import zio.blocks.context._
12
34
 
13
35
  case class Config(debug: Boolean)
36
+ case class Logger(name: String)
14
37
  case class Metrics(count: Int)
38
+ ```
39
+
40
+ We can create and retrieve values by type:
15
41
 
16
- // Create a context with multiple values
17
- val ctx: Context[Config & Metrics] = Context(
42
+ ```scala
43
+ val ctx: Context[Config & Logger & Metrics] = Context(
18
44
  Config(debug = true),
45
+ Logger("app"),
19
46
  Metrics(count = 42)
20
47
  )
48
+ // ctx: Context[Config & Logger & Metrics] = Context(repl.MdocSession.MdocApp.Metrics -> Metrics(42), repl.MdocSession.MdocApp.Logger -> Logger(app), repl.MdocSession.MdocApp.Config -> Config(true))
21
49
 
22
- // Retrieve values by type
23
50
  val config: Config = ctx.get[Config]
24
- val metrics: Metrics = ctx.get[Metrics]
51
+ // config: Config = Config(true)
52
+ val logger: Logger = ctx.get[Logger]
53
+ // logger: Logger = Logger("app")
54
+ ```
55
+
56
+ This ASCII diagram shows how Context maps types to values:
57
+
58
+ ```
59
+ ┌─────────────────────────────────┐
60
+ │ Context[R] │
61
+ │ (Type-Indexed Store) │
62
+ ├─────────────────────────────────┤
63
+ │ Config → Config(true) │
64
+ │ Logger → Logger("app") │
65
+ │ Metrics → Metrics(42) │
66
+ ├─────────────────────────────────┤
67
+ │ ✓ Type-safe retrieval │
68
+ │ ✓ No casting │
69
+ │ ✓ Cache-aware (O(1) repeats) │
70
+ └─────────────────────────────────┘
71
+ ```
72
+
73
+ ## Motivation
74
+
75
+ When building modular applications, we often need to pass multiple dependencies around—a database connection, a config object, a logger, and so on. Existing approaches each have limitations:
76
+
77
+ **`Map[Class[_], Any]`** — no compile-time safety. You must cast the result and remember which keys you registered:
78
+
79
+ ```scala
80
+ // Unsafe approach — easy to make mistakes
81
+ val deps = scala.collection.mutable.Map[Class[_], Any]()
82
+ deps(classOf[Config]) = Config(debug = true)
83
+ deps(classOf[Logger]) = Logger("app")
84
+
85
+ val config = deps(classOf[Config]).asInstanceOf[Config] // Manual cast
86
+ val db = deps(classOf[Database]) // Runtime error if missing
87
+ ```
88
+
89
+ **ZIO's `ZEnvironment`** — type-safe but requires the full ZIO effect system:
90
+
91
+ ```scala
92
+ // Requires ZIO context
93
+ import zio._
94
+
95
+ val makeEnv = for {
96
+ config <- ZIO.service[Config]
97
+ logger <- ZIO.service[Logger]
98
+ } yield (config, logger)
99
+ ```
100
+
101
+ **Context** — combines compile-time type safety with synchronous, pure code:
102
+
103
+ ```scala
104
+ import zio.blocks.context._
105
+
106
+ case class Config(debug: Boolean)
107
+ case class Logger(name: String)
108
+
109
+ // Type-safe, no effects needed
110
+ val ctx = Context(Config(true), Logger("app"))
111
+ val config = ctx.get[Config] // Compile-time proof it exists
112
+ ```
113
+
114
+ ## Installation
115
+
116
+ Add the ZIO Blocks Context module to your `build.sbt`:
117
+
118
+ ```scala
119
+ libraryDependencies += "dev.zio" %% "zio-blocks-context" % "0.0.29"
25
120
  ```
26
121
 
27
122
  ## Construction
28
123
 
29
- Create contexts using overloaded `Context.apply` (supports up to 10 values):
124
+ Context provides several ways to create instances. Choose the approach that best fits your use case: start empty and add values incrementally, or construct a fully-populated context directly with `apply`.
125
+
126
+ ### Creating Empty Contexts
127
+
128
+ Use `Context.empty` to create an empty context with no entries:
129
+
130
+ ```scala
131
+ import zio.blocks.context._
132
+
133
+ val emptyCtx: Context[Any] = Context.empty
134
+ // emptyCtx: Context[Any] = Context()
135
+ val isEmpty = emptyCtx.isEmpty
136
+ // isEmpty: Boolean = true
137
+ ```
138
+
139
+ An empty context has type `Context[Any]` and represents no stored dependencies. This is a useful starting point for incremental construction.
140
+
141
+ ### Creating Multi-Value Contexts with `Context.apply`
142
+
143
+ `Context.apply` is overloaded to accept 1–10 values and returns a context with type `Context[A1 & A2 & ...]`, reflecting all stored types.
144
+
145
+ #### Single Value
146
+
147
+ Create a context with one value:
148
+
149
+ ```scala
150
+ case class Config(debug: Boolean)
151
+ ```
152
+
153
+ With `Config` defined, we can create a single-value context:
30
154
 
31
155
  ```scala
32
- val ctx1 = Context(value1) // Context[Type1]
33
- val ctx2 = Context(value1, value2) // Context[Type1 & Type2]
34
- val ctx3 = Context(v1, v2, v3, v4, v5) // Context[T1 & T2 & T3 & T4 & T5]
156
+ val single: Context[Config] = Context(Config(debug = true))
157
+ // single: Context[Config] = Context(repl.MdocSession.MdocApp1.Config -> Config(true))
35
158
  ```
36
159
 
37
- Or build incrementally from empty:
160
+ #### Multiple Values
161
+
162
+ Create a context with multiple values—the type parameter automatically becomes an intersection of all stored types:
163
+
164
+ ```scala
165
+ case class Logger(name: String)
166
+ ```
167
+
168
+ With `Config` and `Logger` in scope, we can create a multi-value context:
169
+
170
+ ```scala
171
+ val multi: Context[Config & Logger] = Context(
172
+ Config(debug = true),
173
+ Logger("myapp")
174
+ )
175
+ // multi: Context[Config & Logger] = Context(repl.MdocSession.MdocApp1.Logger -> Logger(myapp), repl.MdocSession.MdocApp1.Config -> Config(true))
176
+ ```
177
+
178
+ ### Building Incrementally with `Context#add`
179
+
180
+ For contexts that grow over time, use `Context#add` to build incrementally from an empty context. This is useful when dependencies become available at different points in your initialization:
38
181
 
39
182
  ```scala
40
183
  val ctx = Context.empty
41
- .add(Config(debug = true))
42
- .add(Metrics(count = 0))
43
- // Type: Context[Config & Metrics]
184
+ .add(Config(debug = false))
185
+ .add(Logger("init"))
186
+ // ctx: Context[Config & Logger] = Context(repl.MdocSession.MdocApp1.Logger -> Logger(init), repl.MdocSession.MdocApp1.Config -> Config(false))
187
+ ```
188
+
189
+ The context accumulates all added entries:
190
+
191
+ ```scala
192
+ val size1 = ctx.size
193
+ // size1: Int = 2
44
194
  ```
45
195
 
46
- ## Retrieving Values
196
+ **When to use `Context#add` vs. `Context.apply`:**
197
+ - Use `Context.apply` when you know all dependencies upfront and can construct them together
198
+ - Use `Context#add` when dependencies are added incrementally or conditionally
199
+
200
+ ## Core Operations
201
+
202
+ Context supports inspection, retrieval, and modification operations. All methods are type-safe and leverage the phantom type `R` to track what's stored.
47
203
 
48
- ### get
204
+ ### Inspection
49
205
 
50
- Retrieves a value by type. The type must be in `R`:
206
+ The following methods let you check the contents of a context without retrieving specific values:
207
+
208
+ #### `Context#size`
209
+
210
+ Returns the number of entries in the context:
51
211
 
52
212
  ```scala
53
- val config: Config = ctx.get[Config]
213
+ case class Config(debug: Boolean)
214
+ case class Logger(name: String)
215
+
216
+ val ctx = Context(Config(true), Logger("app"))
54
217
  ```
55
218
 
56
- Supertypes work too:
219
+ Get the number of entries in the context:
220
+
221
+ ```scala
222
+ val sz = ctx.size
223
+ // sz: Int = 2
224
+ ```
225
+
226
+ #### `Context#isEmpty`
227
+
228
+ Returns `true` if the context contains no entries:
57
229
 
58
230
  ```scala
59
- trait Named { def name: String }
60
- case class Person(name: String, age: Int) extends Named
231
+ case class Config(debug: Boolean)
61
232
 
62
- val ctx = Context(Person("Alice", 30))
63
- val named: Named = ctx.get[Named] // Returns the Person
233
+ val empty = Context.empty
234
+ val notEmpty = Context(Config(true))
64
235
  ```
65
236
 
66
- ### getOption
237
+ Now check the `isEmpty` status of both contexts:
238
+
239
+ ```scala
240
+ val e1 = empty.isEmpty
241
+ // e1: Boolean = true
242
+ val e2 = notEmpty.isEmpty
243
+ // e2: Boolean = false
244
+ ```
245
+
246
+ #### `Context#nonEmpty`
247
+
248
+ Returns `true` if the context contains at least one entry (opposite of `isEmpty`):
249
+
250
+ ```scala
251
+ case class Config(debug: Boolean)
252
+
253
+ val empty = Context.empty
254
+ val notEmpty = Context(Config(true))
255
+ ```
67
256
 
68
- Retrieves a value if present, without requiring the type to be in `R`:
257
+ Check the `nonEmpty` status of both contexts:
69
258
 
70
259
  ```scala
71
- val maybeConfig: Option[Config] = ctx.getOption[Config] // Some(...)
72
- val maybeOther: Option[Other] = ctx.getOption[Other] // None
260
+ val e1 = empty.nonEmpty
261
+ // e1: Boolean = false
262
+ val e2 = notEmpty.nonEmpty
263
+ // e2: Boolean = true
73
264
  ```
74
265
 
75
- ## Modifying Contexts
266
+ ### Retrieval
267
+
268
+ The following methods let you retrieve values from a context by type:
269
+
270
+ #### `Context#get`
271
+
272
+ Retrieves a value by type. The type bound `A >: R` ensures that a value of type `A` (or a subtype of `A`) is present at compile time:
76
273
 
77
- ### add
274
+ ```scala
275
+ import zio.blocks.context._
276
+
277
+ case class Config(debug: Boolean)
278
+ case class Logger(name: String)
279
+ case class Metrics(count: Int)
280
+
281
+ val ctx = Context(Config(debug = true), Logger("app"), Metrics(100))
282
+
283
+ // Retrieve by exact type
284
+ val config = ctx.get[Config]
285
+ ```
78
286
 
79
- Adds a value, returning a new context with an expanded type:
287
+ Or by supertype (subtype matching):
80
288
 
81
289
  ```scala
82
- val ctx1 = Context(Config(true)) // Context[Config]
83
- val ctx2 = ctx1.add(Metrics(0)) // Context[Config & Metrics]
290
+ import zio.blocks.context._
291
+
292
+ trait Animal { def sound: String }
293
+ case class Dog(name: String) extends Animal {
294
+ def sound = "Woof"
295
+ }
296
+
297
+ val ctxDog = Context(Dog("Buddy"))
298
+ val animal = ctxDog.get[Animal]
84
299
  ```
85
300
 
86
- Adding a value of an existing type replaces it:
301
+ If you attempt to retrieve a type that is not in the context, the code will not compile because the type bound `A >: R` requires it to be present:
87
302
 
88
303
  ```scala
89
- val ctx1 = Context(Config(debug = false))
90
- val ctx2 = ctx1.add(Config(debug = true))
91
- ctx2.get[Config].debug // true
304
+ // This is a compile-time error, not a runtime error:
305
+ // val metrics: String = ctx.get[String] // Error: String is not in context type
92
306
  ```
93
307
 
94
- ### update
308
+ #### `Context#getOption`
95
309
 
96
- Transforms an existing value:
310
+ Retrieves a value if present, returning `Option[A]`. Unlike `get`, this method does not require the type to be in the context's type parameter. Use it for optional lookups:
97
311
 
98
312
  ```scala
99
- val ctx = Context(Metrics(count = 0))
100
- val updated = ctx.update[Metrics](m => m.copy(count = m.count + 1))
101
- updated.get[Metrics].count // 1
313
+ case class Config(debug: Boolean)
314
+
315
+ val ctx = Context(Config(debug = true))
102
316
  ```
103
317
 
104
- ### ++ (union)
318
+ Try to retrieve both an existing type and a missing type:
105
319
 
106
- Combines two contexts. Right values override left:
320
+ ```scala
321
+ val found: Option[Config] = ctx.getOption[Config]
322
+ // found: Option[Config] = Some(Config(true))
323
+ val missing: Option[String] = ctx.getOption[String]
324
+ // missing: Option[String] = None
325
+ ```
326
+
327
+ ### Modification
328
+
329
+ All modification methods return a new `Context`—the original remains immutable:
330
+
331
+ #### `Context#add`
332
+
333
+ Adds a value to the context, expanding the phantom type by `& A`:
334
+
335
+ ```scala
336
+ import zio.blocks.context._
337
+
338
+ case class Config(debug: Boolean)
339
+ case class Logger(name: String)
340
+
341
+ val ctx1 = Context(Config(true))
342
+ val ctx2 = ctx1.add(Logger("new"))
343
+ ```
344
+
345
+ If a value of the same type already exists, it is replaced:
346
+
347
+ ```scala
348
+ import zio.blocks.context._
349
+
350
+ case class Config(debug: Boolean)
351
+ case class Logger(name: String)
352
+
353
+ val ctx1 = Context(Config(true))
354
+ val ctx2 = ctx1.add(Logger("new"))
355
+ val ctx3 = ctx2.add(Config(debug = false))
356
+ val replaced = ctx3.get[Config]
357
+ ```
358
+
359
+ #### `Context#update`
360
+
361
+ Transforms an existing value if it is present. If the type is not found, the context is returned unchanged:
362
+
363
+ ```scala
364
+ import zio.blocks.context._
365
+
366
+ case class Metrics(count: Int)
367
+
368
+ val ctx = Context(Metrics(count = 10))
369
+ val updated = ctx.update[Metrics](m => m.copy(count = m.count + 5))
370
+ val newCount = updated.get[Metrics].count
371
+ ```
372
+
373
+ #### `Context#++ (Union)`
374
+
375
+ Combines two contexts into a new context containing all entries. When both contexts contain the same type, the value from the right side (second argument) wins:
376
+
377
+ ```scala
378
+ import zio.blocks.context._
379
+
380
+ case class Config(debug: Boolean)
381
+ case class Logger(name: String)
382
+ case class Metrics(count: Int)
383
+
384
+ val left = Context(Config(debug = false), Logger("left"))
385
+ val right = Context(Config(debug = true), Metrics(99))
386
+ val merged = left ++ right
387
+ ```
388
+
389
+ #### `Context#prune`
390
+
391
+ Narrows a context to contain only specified types. All other entries are discarded:
107
392
 
108
393
  ```scala
109
- val ctx1 = Context(Config(debug = false))
110
- val ctx2 = Context(Config(debug = true), Metrics(0))
394
+ import zio.blocks.context._
395
+
396
+ case class Config(debug: Boolean)
397
+ case class Logger(name: String)
398
+ case class Metrics(count: Int)
111
399
 
112
- val merged = ctx1 ++ ctx2
113
- // Config comes from ctx2 (right wins)
400
+ val full = Context(Config(true), Logger("app"), Metrics(100))
401
+ val justConfig = full.prune[Config]
402
+ val configSize = justConfig.size
114
403
  ```
115
404
 
116
- ### prune
405
+ #### `Context#toString`
406
+
407
+ Returns a human-readable representation of the context showing all type-value pairs:
408
+
409
+ ```scala
410
+ case class Config(debug: Boolean)
411
+ case class Logger(name: String)
412
+
413
+ val ctx = Context(Config(debug = true), Logger("app"))
414
+ ```
117
415
 
118
- Narrows a context to specific types:
416
+ Convert the context to a human-readable string representation:
119
417
 
120
418
  ```scala
121
- val ctx: Context[Config & Metrics & Other] = ...
122
- val pruned: Context[Config] = ctx.prune[Config]
419
+ val str = ctx.toString
420
+ // str: String = "Context(repl.MdocSession.MdocApp1.Logger -> Logger(app), repl.MdocSession.MdocApp1.Config -> Config(true))"
123
421
  ```
124
422
 
125
423
  ## Covariance
126
424
 
127
- `Context` is covariant, so `Context[Specific]` is a subtype of `Context[General]`:
425
+ `Context` is covariant in its type parameter, meaning `Context[Dog] <: Context[Animal]` when `Dog <: Animal`. This allows passing a more-specific context to code expecting a more-general one:
128
426
 
129
427
  ```scala
130
- def process(ctx: Context[Named]): Unit = {
131
- val named = ctx.get[Named]
132
- println(named.name)
428
+ import zio.blocks.context._
429
+
430
+ trait Animal { def sound: String }
431
+ case class Dog(name: String) extends Animal {
432
+ def sound = "Woof"
133
433
  }
134
434
 
135
- val ctx: Context[Person] = Context(Person("Bob", 25))
136
- process(ctx) // Works: Context[Person] <: Context[Named]
435
+ def processAnimal(ctx: Context[Animal]): String = ctx.get[Animal].sound
436
+
437
+ val dogCtx = Context(Dog("Buddy"))
438
+ val sound = processAnimal(dogCtx)
137
439
  ```
138
440
 
139
- ## Type Safety: IsNominalType
441
+ Covariance also applies during retrieval—if you request a supertype, the stored subtype is returned.
140
442
 
141
- Only nominal types can be stored. The `IsNominalType[A]` typeclass is derived automatically for:
443
+ ## Type Safety: IsNominalType
142
444
 
143
- - Classes, case classes, traits, objects
144
- - Enums (Scala 3)
145
- - Applied types (`List[Int]`, `Map[K, V]`)
445
+ Context only accepts **nominal types**—concrete classes, case classes, traits, and objects. The compiler automatically derives `IsNominalType[A]` for allowed types and rejects unsupported kinds:
146
446
 
147
- Not supported (compile error):
447
+ **Supported:**
448
+ - Classes: `case class Config(...)`
449
+ - Traits: `trait Logger`
450
+ - Objects: `object Registry`
451
+ - Applied types: `List[Int]`, `Map[String, Int]`
452
+ - Enums (Scala 3): `enum Color { case Red, Green, Blue }`
148
453
 
149
- - Intersection types: `A & B`
150
- - Union types: `A | B`
454
+ **Not supported (compile error):**
455
+ - Intersection types: `A & B` (use the context type parameter instead)
456
+ - Union types: `A | B`
151
457
  - Structural types: `{ def foo: Int }`
152
458
 
459
+ Attempting to store an unsupported type:
460
+
461
+ ```scala
462
+ // This fails at compile time:
463
+ // Context.empty.add(null: (String & Int)) // Error: unsupported type
464
+ ```
465
+
153
466
  ## Performance
154
467
 
155
- - **Caching**: Retrieved values are cached for O(1) subsequent lookups
156
- - **Subtype matching**: Supertype lookups find matching subtypes and cache results
157
- - **Platform-optimized**: JVM uses `ConcurrentHashMap`; JS uses efficient mutable maps
468
+ **Caching**: When a value is retrieved via `get` or `getOption`, the result is cached. Repeated lookups for the same type return the cached value in O(1) time without traversing the entries again.
469
+
470
+ **Subtype matching**: Supertype lookups scan the entry list linearly to find a compatible subtype. After the first lookup, the result is cached, so subsequent requests for that supertype are O(1).
471
+
472
+ **Platform optimizations**: The JVM implementation uses `ConcurrentHashMap` for thread-safe caching. The JavaScript platform uses a simpler in-memory mutable hash map for efficient lookups.
473
+
474
+ ## Comparing Approaches
475
+
476
+ Here is a comparison of Context with related alternatives:
477
+
478
+ | Feature | `Map[Class[_], Any]` | `ZEnvironment` | `Context` |
479
+ |---------------------|----------------------|------------------|-----------|
480
+ | Type-safe retrieval | ✗ (cast required) | ✓ | ✓ |
481
+ | Compile-time proof | ✗ | ✓ | ✓ |
482
+ | Effect-free | ✓ | ✗ (requires ZIO) | ✓ |
483
+ | Immutable | ✓ | ✓ | ✓ |
484
+ | Cached lookups | ✗ | ✓ | ✓ |
485
+ | Supertype matching | ✗ | ✓ | ✓ |
486
+
487
+ ## Integration with Wire and Scope
488
+
489
+ `Context` is the dependency carrier in ZIO Blocks' Wire-based dependency injection system. A `Wire[-In, +Out]` describes how to build an output given input dependencies, and contexts supply those dependencies. Wire and Scope work together to provide type-safe, compile-checked dependency injection:
490
+
491
+ ```scala
492
+ // Pseudocode illustrating how Context integrates with Wire and Scope
493
+ import zio.blocks.scope._
494
+ import zio.blocks.context._
495
+
496
+ case class Config(debug: Boolean)
497
+ case class Logger(name: String)
498
+ case class Service(config: Config, logger: Logger)
499
+
500
+ // Define a wire that requires Config and Logger to build Service
501
+ val buildService = Wire.make[Config & Logger, Service]
502
+
503
+ // Create a context with the required dependencies
504
+ val deps = Context(Config(debug = true), Logger("app"))
505
+
506
+ // Create a scope and instantiate the service
507
+ Scope.global.scoped { scope =>
508
+ val service: Service = buildService.make(scope, deps)
509
+ }
510
+ ```
511
+
512
+ ## Running the Examples
513
+
514
+ All code from this guide is available as runnable examples in the `schema-examples` module.
515
+
516
+ **1. Clone the repository and navigate to the project:**
517
+
518
+ ```bash
519
+ git clone https://github.com/zio/zio-blocks.git
520
+ cd zio-blocks
521
+ ```
522
+
523
+ **2. Run individual examples with sbt:**
524
+
525
+ **Context construction: creating contexts with apply, empty.add, and inspecting size/isEmpty/nonEmpty**
526
+
527
+ ```scala title="schema-examples/src/main/scala/context/ContextConstructionExample.scala"
528
+ package context
529
+
530
+ import zio.blocks.context._
531
+ import util.ShowExpr.show
532
+
533
+ // Context.empty creates an empty, type-safe dependency container.
534
+ // Use Context.apply(...) to construct contexts with 1–10 values.
535
+ // The phantom type parameter tracks which types are present.
536
+ object ContextConstructionExample extends App {
537
+
538
+ case class Config(debug: Boolean)
539
+ case class Logger(name: String)
540
+ case class Metrics(count: Int)
541
+
542
+ // Start with an empty context.
543
+ show(Context.empty.size)
544
+ show(Context.empty.isEmpty)
545
+
546
+ // Create a context with one value.
547
+ val ctx1 = Context(Config(debug = true))
548
+ show(ctx1.size)
549
+ show(ctx1.nonEmpty)
550
+
551
+ // Create a context with multiple values (up to 10 supported).
552
+ val ctx2 = Context(
553
+ Config(debug = false),
554
+ Logger("myapp")
555
+ )
556
+ show(ctx2.size)
557
+ show(ctx2.isEmpty)
558
+
559
+ // Create a larger context.
560
+ val ctx3 = Context(
561
+ Config(debug = true),
562
+ Logger("prod"),
563
+ Metrics(count = 100)
564
+ )
565
+ show(ctx3.size)
566
+
567
+ // Build incrementally from empty using add.
568
+ val ctxBuilt = Context.empty
569
+ .add(Config(debug = false))
570
+ .add(Logger("init"))
571
+ .add(Metrics(count = 0))
572
+ show(ctxBuilt.size)
573
+ show(ctxBuilt.nonEmpty)
574
+
575
+ // toString shows the context contents in a readable format.
576
+ show(ctx3.toString)
577
+ }
578
+ ```
579
+
580
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/context/ContextConstructionExample.scala))
581
+
582
+ ```bash
583
+ sbt "schema-examples/runMain context.ContextConstructionExample"
584
+ ```
585
+
586
+ **Context retrieval: using get, supertype lookups, and getOption for safe access**
587
+
588
+ ```scala title="schema-examples/src/main/scala/context/ContextRetrievalExample.scala"
589
+ package context
590
+
591
+ import zio.blocks.context._
592
+ import util.ShowExpr.show
593
+
594
+ // Context#get[A] retrieves a value by type with compile-time proof of existence.
595
+ // Context#getOption[A] retrieves a value if present, returning None if missing.
596
+ // Supertype matching allows retrieving a subtype using a more general supertype.
597
+ object ContextRetrievalExample extends App {
598
+
599
+ case class Config(debug: Boolean)
600
+ case class Logger(name: String)
601
+ case class Metrics(count: Int)
602
+
603
+ trait Animal { def sound: String }
604
+ case class Dog(name: String) extends Animal {
605
+ def sound = "Woof"
606
+ }
607
+
608
+ // Create a context with multiple values.
609
+ val ctx = Context(
610
+ Config(debug = true),
611
+ Logger("app"),
612
+ Metrics(count = 42)
613
+ )
614
+
615
+ // Retrieve by exact type.
616
+ val config: Config = ctx.get[Config]
617
+ show(config)
618
+
619
+ val logger: Logger = ctx.get[Logger]
620
+ show(logger)
621
+
622
+ val metrics: Metrics = ctx.get[Metrics]
623
+ show(metrics)
624
+
625
+ // getOption returns Some if the type is present.
626
+ val configOpt: Option[Config] = ctx.getOption[Config]
627
+ show(configOpt)
628
+
629
+ // getOption returns None if the type is missing (safe for optional lookups).
630
+ val missingOpt: Option[String] = ctx.getOption[String]
631
+ show(missingOpt)
632
+
633
+ // Supertype matching: retrieve a subtype using a supertype.
634
+ val dogCtx = Context(Dog("Buddy"))
635
+ val animal: Animal = dogCtx.get[Animal]
636
+ show(animal.sound)
637
+
638
+ // getOption also supports supertype matching.
639
+ val animalOpt: Option[Animal] = dogCtx.getOption[Animal]
640
+ show(animalOpt.map(_.sound))
641
+
642
+ // Cache efficiency: repeated lookups return the cached instance.
643
+ val first = ctx.get[Logger]
644
+ val second = ctx.get[Logger]
645
+ show(first eq second)
646
+ }
647
+ ```
648
+
649
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/context/ContextRetrievalExample.scala))
650
+
651
+ ```bash
652
+ sbt "schema-examples/runMain context.ContextRetrievalExample"
653
+ ```
654
+
655
+ **Context modification: adding values, updating existing ones, merging contexts, and pruning types**
656
+
657
+ ```scala title="schema-examples/src/main/scala/context/ContextModificationExample.scala"
658
+ package context
659
+
660
+ import zio.blocks.context._
661
+ import util.ShowExpr.show
662
+
663
+ // Context is immutable; modification methods return new contexts.
664
+ // Context#add expands the context with a new value (or replaces if type exists).
665
+ // Context#update transforms a value if present.
666
+ // Context#++ merges two contexts (right side wins on conflict).
667
+ // Context#prune narrows to specific types.
668
+ object ContextModificationExample extends App {
669
+
670
+ case class Config(debug: Boolean)
671
+ case class Logger(name: String)
672
+ case class Metrics(count: Int)
673
+
674
+ // Start with a simple context.
675
+ val ctx1 = Context(Config(debug = false))
676
+ show(ctx1.size)
677
+
678
+ // add expands the context with a new value.
679
+ val ctx2 = ctx1.add(Logger("app"))
680
+ show(ctx2.size)
681
+ show(ctx2.get[Config])
682
+ show(ctx2.get[Logger])
683
+
684
+ // add replaces if the type already exists.
685
+ val ctx3 = ctx2.add(Config(debug = true))
686
+ show(ctx3.size)
687
+ show(ctx3.get[Config].debug)
688
+
689
+ // update transforms a value if present.
690
+ val ctx4 = ctx3.add(Metrics(count = 100))
691
+ val updated = ctx4.update[Metrics](m => m.copy(count = m.count + 50))
692
+ show(updated.get[Metrics].count)
693
+
694
+ // update also works on existing types.
695
+ val configUpdated = updated.update[Config](c => c.copy(debug = false))
696
+ show(configUpdated.get[Config])
697
+
698
+ // ++ merges two contexts; right side wins on conflict.
699
+ val left = Context(Config(debug = false), Logger("left"))
700
+ val right = Context(Config(debug = true), Metrics(count = 99))
701
+ val merged = left ++ right
702
+ show(merged.size)
703
+ show(merged.get[Config].debug)
704
+ show(merged.get[Logger].name)
705
+ show(merged.get[Metrics].count)
706
+
707
+ // prune narrows a context to specific types.
708
+ val full = Context(Config(true), Logger("app"), Metrics(100))
709
+ show(full.size)
710
+ val justConfig = full.prune[Config]
711
+ show(justConfig.size)
712
+ show(justConfig.getOption[Logger])
713
+
714
+ // Chaining modifications.
715
+ val chain = Context.empty
716
+ .add(Config(debug = true))
717
+ .add(Logger("chain"))
718
+ .add(Metrics(count = 0))
719
+ .update[Metrics](m => m.copy(count = 42))
720
+ show(chain.size)
721
+ show(chain.get[Metrics].count)
722
+ }
723
+ ```
724
+
725
+ ([source](https://github.com/zio/zio-blocks/blob/main/schema-examples/src/main/scala/context/ContextModificationExample.scala))
726
+
727
+ ```bash
728
+ sbt "schema-examples/runMain context.ContextModificationExample"
729
+ ```