@zio.dev/zio-blocks 0.0.21 → 0.0.24

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.
@@ -0,0 +1,361 @@
1
+ ---
2
+ id: lazy
3
+ title: "Lazy"
4
+ ---
5
+
6
+ The `Lazy[A]` data type represents a deferred computation that produces a value of type `A`. Unlike Scala's built-in `lazy val`, ZIO Blocks' `Lazy` provides a powerful, monadic abstraction with explicit error handling, memoization, and stack-safe evaluation through trampolining:
7
+
8
+ ```scala
9
+ sealed trait Lazy[+A] {
10
+ def force: A
11
+ def isEvaluated: Boolean
12
+ def map[B](f: A => B): Lazy[B]
13
+ def flatMap[B](f: A => Lazy[B]): Lazy[B]
14
+ // ... more operations
15
+ }
16
+
17
+ object Lazy {
18
+ def apply[A](expression: => A): Lazy[A]
19
+ def fail(throwable: Throwable): Lazy[Nothing]
20
+ // ... more constructors
21
+ }
22
+ ```
23
+
24
+ Once a `Lazy` computation is evaluated, the result is cached and `isEvaluated` becomes `true`. Subsequent calls to `force` return the cached value without re-executing the computation, as long as the computed result is not `null`.
25
+
26
+ Note: the current implementation uses `null` internally as the sentinel for “not evaluated”. If a `Lazy` computation legitimately returns `null`, it will be recomputed on every `force` call and `isEvaluated` will remain `false`. To benefit from memoization, prefer non-null results (for example, use `Option[A]` instead of returning `null`).
27
+ The `force` method uses trampolining (an explicit stack) to evaluate deeply nested `Lazy` computations without risking stack overflow.
28
+
29
+ ## Why Lazy Exists?
30
+
31
+ During type-class derivation, instances for nested types must be created before they are used. `Lazy` allows the derivation machinery to build a structure of deferred computations, resolving them only when the final instance is forced.
32
+
33
+ When deriving a type-class instance for a complex type, the derivation machinery needs to:
34
+
35
+ 1. **Traverse the schema tree**: Visit each node (records, variants, sequences, etc.)
36
+ 2. **Create instances for nested types**: Before creating an instance for a parent type, instances for child types must exist
37
+ 3. **Handle recursion**: For recursive types, the instance for the recursive reference must be deferred
38
+
39
+ Here's a simplified view of how `Lazy` enables this:
40
+
41
+ ```scala
42
+ // In DerivationBuilder
43
+ def transformRecord[A](
44
+ fields: IndexedSeq[Term[F, A, ?]],
45
+ metadata: F[BindingType.Record, A],
46
+ // ...
47
+ ): Lazy[Reflect.Record[G, A]] = Lazy {
48
+ // Get instances for field types (may trigger evaluation of other Lazy values)
49
+ val fieldInstances = fields.map { field =>
50
+ D.instance(field.value.metadata) // Returns Lazy[TC[FieldType]]
51
+ }
52
+
53
+ // Create the record instance
54
+ val instance = deriver.deriveRecord(fields, /* ... */)
55
+
56
+ new Reflect.Record(fields, ..., new BindingInstance(metadata, instance), ...)
57
+ }
58
+ ```
59
+
60
+ The `BindingInstance` class wraps both a `Binding` and a `Lazy[TC[A]]`:
61
+
62
+ ```scala
63
+ final case class BindingInstance[TC[_], T, A](
64
+ binding: Binding[T, A],
65
+ instance: Lazy[TC[A]]
66
+ )
67
+ ```
68
+
69
+ This allows the derivation to build a complete tree of `Lazy` instances, which are only forced when the final type-class instance is needed.
70
+
71
+ ## Creating Lazy Values
72
+
73
+ ### Basic Construction
74
+
75
+ Create a `Lazy` value by passing a by-name expression to `Lazy.apply`:
76
+
77
+ ```scala
78
+ import zio.blocks.schema.Lazy
79
+
80
+ // The expression is NOT evaluated here
81
+ val lazyInt: Lazy[Int] = Lazy {
82
+ println("Computing...")
83
+ 42
84
+ }
85
+
86
+ println(lazyInt.isEvaluated) // false
87
+
88
+ // Now the expression is evaluated
89
+ val result = lazyInt.force // prints "Computing..."
90
+ println(result) // 42
91
+ println(lazyInt.isEvaluated) // true
92
+
93
+ // Subsequent calls return the cached value
94
+ val result2 = lazyInt.force // does NOT print "Computing..."
95
+ println(result2) // 42
96
+ ```
97
+
98
+ ### Creating Failed Lazy Values
99
+
100
+ Use `Lazy.fail` to create a `Lazy` that will throw an exception when forced:
101
+
102
+ ```scala
103
+ val failingLazy: Lazy[Int] =
104
+ Lazy.fail(new RuntimeException("Something went wrong"))
105
+ ```
106
+
107
+ ## Transforming Lazy Values
108
+
109
+ `Lazy` is a monad, supporting `map`, `flatMap`, and other familiar operations.
110
+
111
+ ### map
112
+
113
+ Transform the value inside a `Lazy` without forcing it:
114
+
115
+ ```scala
116
+ val lazyInt: Lazy[Int] = Lazy(42)
117
+ val lazyString: Lazy[String] = lazyInt.map(_.toString)
118
+
119
+ println(lazyString.force) // "42"
120
+ ```
121
+
122
+ ### flatMap
123
+
124
+ Chain `Lazy` computations together:
125
+
126
+ ```scala
127
+ val lazyA: Lazy[Int] = Lazy(10)
128
+ val lazyB: Lazy[Int] = Lazy(20)
129
+
130
+ val lazySum: Lazy[Int] = lazyA.flatMap(a => lazyB.map(b => a + b))
131
+
132
+ println(lazySum.force) // 30
133
+ ```
134
+
135
+ Using for-comprehension syntax:
136
+
137
+ ```scala
138
+ val result: Lazy[String] = for {
139
+ x <- Lazy(10)
140
+ y <- Lazy(20)
141
+ z <- Lazy(30)
142
+ } yield s"Sum: ${x + y + z}"
143
+
144
+ println(result.force) // "Sum: 60"
145
+ ```
146
+
147
+ ### as
148
+
149
+ Replace the value with a constant, discarding the original:
150
+
151
+ ```scala
152
+ val lazy42: Lazy[Int] = Lazy(42)
153
+ val lazyHello: Lazy[String] = lazy42.as("Hello")
154
+
155
+ println(lazyHello.force) // "Hello"
156
+ ```
157
+
158
+ ### unit
159
+
160
+ Discard the value, keeping only the side effects:
161
+
162
+ ```scala
163
+ val lazyWithSideEffect: Lazy[Int] = Lazy {
164
+ println("Side effect!")
165
+ 42
166
+ }
167
+
168
+ val lazyUnit: Lazy[Unit] = lazyWithSideEffect.unit
169
+ lazyUnit.force // prints "Side effect!", returns ()
170
+ ```
171
+
172
+ ### flatten
173
+
174
+ Flatten a nested `Lazy[Lazy[A]]` into `Lazy[A]`:
175
+
176
+ ```scala
177
+ val nested: Lazy[Lazy[Int]] = Lazy(Lazy(42))
178
+ val flat: Lazy[Int] = nested.flatten
179
+
180
+ println(flat.force) // 42
181
+ ```
182
+
183
+ ### zip
184
+
185
+ Combine two `Lazy` values into a tuple:
186
+
187
+ ```scala
188
+ val lazyA: Lazy[Int] = Lazy(1)
189
+ val lazyB: Lazy[String] = Lazy("hello")
190
+
191
+ val lazyPair: Lazy[(Int, String)] = lazyA.zip(lazyB)
192
+
193
+ println(lazyPair.force) // (1, "hello")
194
+ ```
195
+
196
+ ## Error Handling
197
+
198
+ ### catchAll
199
+
200
+ Recover from errors by providing an alternative `Lazy`:
201
+
202
+ ```scala
203
+ val failing: Lazy[Int] = Lazy(throw new RuntimeException("oops"))
204
+ val recovered: Lazy[Int] = failing.catchAll(_ => Lazy(0))
205
+
206
+ println(recovered.force) // 0
207
+ ```
208
+
209
+ The error handler receives the thrown exception:
210
+
211
+ ```scala
212
+ val failing: Lazy[Int] = Lazy(throw new RuntimeException("specific error"))
213
+ val handled: Lazy[Int] = failing.catchAll { error =>
214
+ println(s"Caught: ${error.getMessage}")
215
+ Lazy(-1)
216
+ }
217
+
218
+ println(handled.force) // prints "Caught: specific error", returns -1
219
+ ```
220
+
221
+ ### ensuring
222
+
223
+ Run a finalizer regardless of success or failure:
224
+
225
+ ```scala
226
+ var resourceClosed = false
227
+
228
+ val computation: Lazy[Int] = Lazy {
229
+ 42
230
+ }.ensuring(Lazy {
231
+ resourceClosed = true
232
+ })
233
+
234
+ println(computation.force) // 42
235
+ println(resourceClosed) // true
236
+ ```
237
+
238
+ The finalizer runs even when the main computation fails:
239
+
240
+ ```scala
241
+ var finalizerRan = false
242
+
243
+ val failing: Lazy[Int] = Lazy[Int] {
244
+ throw new RuntimeException("error")
245
+ }.ensuring(Lazy {
246
+ finalizerRan = true
247
+ })
248
+
249
+ try {
250
+ failing.force
251
+ } catch {
252
+ case _: RuntimeException => ()
253
+ }
254
+
255
+ println(finalizerRan) // true
256
+ ```
257
+
258
+ ## Working with Collections
259
+
260
+ ### collectAll
261
+
262
+ Convert an `IndexedSeq[Lazy[A]]` into a `Lazy[IndexedSeq[A]]`:
263
+
264
+ ```scala
265
+ val lazies: IndexedSeq[Lazy[Int]] = IndexedSeq(Lazy(1), Lazy(2), Lazy(3))
266
+ val collected: Lazy[IndexedSeq[Int]] = Lazy.collectAll(lazies)
267
+
268
+ println(collected.force) // IndexedSeq(1, 2, 3)
269
+ ```
270
+
271
+ ### foreach
272
+
273
+ Apply a function that returns `Lazy` to each element of a collection:
274
+
275
+ ```scala
276
+ val numbers: IndexedSeq[Int] = IndexedSeq(1, 2, 3)
277
+ val doubled: Lazy[IndexedSeq[Int]] = Lazy.foreach(numbers)(n => Lazy(n * 2))
278
+
279
+ println(doubled.force) // IndexedSeq(2, 4, 6)
280
+ ```
281
+
282
+ ## How `force` Works: A Deep Dive
283
+
284
+ The `force` method is the heart of the `Lazy` data type. It evaluates the deferred computation and returns the result. Understanding how it works is essential for understanding `Lazy`.
285
+
286
+ ### Internal Structure
287
+
288
+ `Lazy` has three internal components:
289
+
290
+ 1. **`Defer[A](thunk: () => A)`**: Represents a deferred computation. The `thunk` is a function that, when called, produces the value.
291
+
292
+ 2. **`FlatMap[A, B](first: Lazy[A], cont: Cont[A, B])`**: Represents a chained computation where `first` must be evaluated, then its result is passed to the continuation `cont`.
293
+
294
+ 3. **`Cont[A, B](ifSuccess: A => Lazy[B], ifError: Throwable => Lazy[B])`**: A continuation that handles both success and error cases.
295
+
296
+ Additionally, each `Lazy` instance has mutable fields for memoization:
297
+ - `value: Any` - Stores the cached successful result
298
+ - `error: Throwable` - Stores the cached exception
299
+
300
+ ### Why Trampolining?
301
+
302
+ Without trampolining, deeply nested `flatMap` chains would overflow the stack:
303
+
304
+ ```scala
305
+ // This would overflow without trampolining
306
+ var lazy: Lazy[Int] = Lazy(0)
307
+ for (_ <- 1 to 100000) {
308
+ lazy = lazy.flatMap(n => Lazy(n + 1))
309
+ }
310
+ lazy.force // Works! Returns 100000
311
+ ```
312
+
313
+ The trampolining approach converts recursive calls into an iterative loop with an explicit stack (`List[Cont[Any, Any]]`), consuming constant stack space regardless of nesting depth.
314
+
315
+ ## Comparison with Scala's `lazy val`
316
+
317
+ | Feature | `Lazy[A]` | Scala `lazy val` |
318
+ |--------------------|------------------------------|------------------------------|
319
+ | Monadic operations | Yes (`map`, `flatMap`) | No |
320
+ | Error handling | Yes (`catchAll`, `ensuring`) | No |
321
+ | Stack safety | Yes (trampolined `flatMap`) | N/A (no monadic chaining) |
322
+ | Composable | Yes | Limited |
323
+
324
+
325
+ ## API Reference
326
+
327
+ ### Constructors
328
+
329
+ | Method | Description |
330
+ |---------------------------|---------------------------------------------------------------------|
331
+ | `Lazy(expr: => A)` | Create a `Lazy` from a by-name expression |
332
+ | `Lazy.fail(e: Throwable)` | Create a `Lazy` that fails with the given exception |
333
+ | `Lazy.collectAll(values)` | Combine an `IndexedSeq` of `Lazy` into a single `Lazy` |
334
+ | `Lazy.foreach(values)(f)` | Apply a function to each element of an `IndexedSeq`, collecting results |
335
+
336
+ Note: For other collection types, convert them using `.toIndexedSeq` before calling `Lazy.collectAll` or `Lazy.foreach`.
337
+ ### Instance Methods
338
+
339
+ | Method | Description |
340
+ |----------------------------------------------|-------------------------------------------|
341
+ | `force: A` | Evaluate and return the result (memoized) |
342
+ | `isEvaluated: Boolean` | Check if the value has been computed |
343
+ | `map[B](f: A => B): Lazy[B]` | Transform the value |
344
+ | `flatMap[B](f: A => Lazy[B]): Lazy[B]` | Chain computations |
345
+ | `flatten: Lazy[B]` | Flatten nested `Lazy` |
346
+ | `as[B](b: => B): Lazy[B]` | Replace the value with a constant |
347
+ | `unit: Lazy[Unit]` | Discard the value |
348
+ | `zip[B](that: Lazy[B]): Lazy[(A, B)]` | Combine with another `Lazy` |
349
+ | `catchAll[B >: A](f: Throwable => Lazy[B]): Lazy[B]` | Handle errors |
350
+ | `ensuring(finalizer: Lazy[Any]): Lazy[A]` | Run finalizer on completion |
351
+
352
+ ### Equality and Hashing
353
+
354
+ `Lazy` values are compared by forcing both values and comparing the results:
355
+
356
+ ```scala
357
+ Lazy(42) == Lazy(42) // true (forces both)
358
+ Lazy(42).hashCode == Lazy(42).hashCode // true
359
+ ```
360
+
361
+ Note: Comparing `Lazy` values will force their evaluation. Also, `hashCode` forces the value and then calls `.hashCode` on it; if a `Lazy` evaluates to `null`, `hashCode` will throw a `NullPointerException`, and repeated equality checks may re-run the thunk because `null` is used internally as the “not yet evaluated” sentinel.