@zio.dev/zio-blocks 0.0.33 → 0.0.51

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 (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -0,0 +1,2526 @@
1
+ ---
2
+ id: stream
3
+ title: "Stream"
4
+ ---
5
+
6
+ import Tabs from '@theme/Tabs';
7
+ import TabItem from '@theme/TabItem';
8
+
9
+ `Stream[+E, +A]` is a **lazy, pull-based, typed-error stream** of elements that may fail with an error of type `E`. Nothing executes until a terminal operation is called. When you run a stream synchronously, you get `Either[E, Z]` — typed errors surface as `Left(e)`, and untyped defects propagate as exceptions:
10
+
11
+ ```scala
12
+ abstract class Stream[+E, +A] {
13
+ def run[E2 >: E, Z](sink: Sink[E2, A, Z]): Either[E2, Z]
14
+ def runCollect: Either[E, Chunk[A]]
15
+ }
16
+ ```
17
+
18
+ `Stream` is purely functional, referentially transparent, and resource-safe:
19
+ - **Lazy**: descriptions of pipelines, not eager computations
20
+ - **Synchronous**: all terminal operations return `Either[E, Z]` directly (no async effects)
21
+ - **Pull-based**: execution is driven from the sink backward through the pipeline
22
+ - **Typed errors**: distinguish recoverable errors (`E`) from untyped defects (`Throwable`)
23
+ - **Resource-safe**: RAII semantics ensure resources are released in all cases
24
+
25
+ ## Motivation
26
+
27
+ Traditional eager sequences (like Scala `List`) fall short in **three critical dimensions**. Here's what `Stream[E, A]` solves for each:
28
+
29
+ **1. Efficiency — Wasteful Computation**
30
+
31
+ With eager evaluation, the entire dataset is processed upfront, regardless of how many elements you actually need. This example shows how much work is wasted:
32
+
33
+ ```scala
34
+ // With Scala List (eager evaluation)
35
+ val data = (1 to 1_000_000).toList
36
+ val result = data
37
+ .map(_ * 2) // eagerly: 1M multiplications
38
+ .filter(_ > 10) // eagerly: 1M comparisons
39
+ .take(10) // finally: keep only 10
40
+ // ❌ Wasted work: computed and discarded 999,990 elements!
41
+ ```
42
+
43
+ The problem: `List` eagerly applies `.map` and `.filter` to all 1 million elements, even though only the first 10 passing elements matter. In data processing pipelines (parsing CSV files, filtering logs, transforming sensor streams), this is enormously wasteful.
44
+
45
+ With `Stream[E, A]`, the architecture is **inverted**: the **sink (consumer) pulls** from the stream. If the sink asks for only 10 elements, only ~20 calculations occur (enough to find 10 valid results after filtering):
46
+
47
+ ```scala
48
+ import zio.blocks.streams.*
49
+
50
+ // With Stream (lazy, pull-based evaluation)
51
+ val result: Either[Nothing, zio.blocks.chunk.Chunk[Int]] =
52
+ Stream.fromRange((1 to 100))
53
+ .map(_ * 2)
54
+ .filter(_ > 10)
55
+ .run(Sink.take(10))
56
+ // ✓ Computation stops after 10 valid elements are produced
57
+ // Only necessary work: ~20 multiplications, ~20 comparisons
58
+ ```
59
+
60
+ This **short-circuiting** behavior is automatic and requires no special syntax.
61
+
62
+ **2. Resource Management — Error-Prone Cleanup**
63
+
64
+ When you open resources (file handles, network connections, database cursors), you must release them in **all** code paths—success, error, and even mid-stream cancellation. With eager sequences, this burden falls on the caller:
65
+
66
+ ```
67
+ // ❌ With traditional Scala (manual resource management, uses var for mutable state)
68
+ import java.io.*
69
+
70
+ var file: BufferedReader = null
71
+ try {
72
+ file = new BufferedReader(new FileReader("build.sbt"))
73
+ var count = 0L
74
+ var char = file.read()
75
+ while (char != -1) {
76
+ if (!Character.isWhitespace(char)) {
77
+ count += 1
78
+ }
79
+ char = file.read()
80
+ }
81
+ count
82
+ } catch {
83
+ case e: IOException =>
84
+ throw e
85
+ } finally {
86
+ if (file != null) file.close() // ✓ Manual cleanup in finally
87
+ }
88
+ // ❌ Problem: You must remember the finally block
89
+ // ❌ Problem: If an exception occurs in the loop, cleanup must still run (easy to forget!)
90
+ // ❌ Problem: Scale to 10 resources? 50 resources? Manually nesting becomes error-prone
91
+ ```
92
+
93
+ With `Stream[E, A]`, resource cleanup is **automatic, composable, and guaranteed**—even on error or if the sink cancels early:
94
+
95
+ ```scala
96
+ import zio.blocks.streams.*
97
+ import java.io.*
98
+
99
+ // With Stream (resource-safe RAII)
100
+ // Open a file and count non-whitespace characters
101
+ val charCount: Either[IOException, Long] =
102
+ Stream
103
+ .fromJavaReader(new FileReader("build.sbt")) // lazily acquires file handle
104
+ .filter(!_.isWhitespace) // process only non-whitespace
105
+ .count // count all matching characters
106
+ // ✓ File automatically closes in finally block (success or error)
107
+ // ✓ If FileReader throws, or filter throws, or count throws—cleanup still runs
108
+ // ✓ No manual try/finally needed; no resource leak risk
109
+ // ✓ Multiple resources (files, connections, etc.) compose naturally
110
+ ```
111
+
112
+ The key difference: `Stream` releases resources via **RAII** (Resource Acquisition Is Initialization) — the resource's lifetime is bound to the compiled stream's `close()` method, which the terminal operation (`run`) always calls in a `finally` block.
113
+
114
+ **3. Error Handling — Untyped Errors**
115
+
116
+ Traditional error handling conflates two categories: recoverable **domain errors** (e.g., parsing failed, validation failed) and fatal **defects** (e.g., `OutOfMemoryError`, `NullPointerException`). This makes it hard to write correct error recovery code:
117
+
118
+ ```scala
119
+ import scala.util.Try
120
+
121
+ // With Try/catch (untyped errors)
122
+ case class ParseError(msg: String)
123
+
124
+ def parseLines(lines: List[String]): Try[List[Int]] = Try {
125
+ lines.map { line =>
126
+ line.toInt // throws NumberFormatException (defect, not domain error!)
127
+ }
128
+ }
129
+
130
+ val result = parseLines(List("1", "abc", "3"))
131
+ result match {
132
+ case util.Success(nums) => println(s"Parsed: $nums")
133
+ case util.Failure(e) =>
134
+ // ❌ Can't tell if 'e' is a parse error or a JVM defect
135
+ // ❌ Must handle *all* exceptions the same way
136
+ // ❌ Domain logic mixed with system-level exception handling
137
+ println(s"Error: $e")
138
+ }
139
+ ```
140
+
141
+ With `Stream[E, A]`, typed errors (`E`) are distinct from untyped defects (`Throwable`), enabling proper error recovery:
142
+
143
+ ```scala
144
+ import zio.blocks.streams.*
145
+
146
+ case class ParseError(msg: String)
147
+
148
+ // With Stream (typed errors)
149
+ val result: Either[ParseError, zio.blocks.chunk.Chunk[Int]] =
150
+ Stream
151
+ .fromIterable(List("1", "abc", "3"))
152
+ .flatMap { line =>
153
+ try {
154
+ Stream.succeed(line.toInt) // success path
155
+ } catch {
156
+ case _: NumberFormatException =>
157
+ Stream.fail(ParseError(s"Not a number: $line")) // typed error
158
+ }
159
+ }
160
+ .runCollect
161
+
162
+ result match {
163
+ case Left(parseError) =>
164
+ // ✓ This branch is *only* for domain errors we chose to surface
165
+ println(s"Parse error: ${parseError.msg}")
166
+ case Right(nums) =>
167
+ // ✓ Untyped defects (OutOfMemoryError, etc.) propagate as exceptions
168
+ // ✓ Clear separation: Either[E, Z] is for recovery, uncaught exceptions are fatal
169
+ println(s"Parsed: ${nums}")
170
+ }
171
+ ```
172
+
173
+ The key distinction: `Either[ParseError, Z]` means domain errors are *recoverable* via `Left`; any uncaught `Throwable` defect propagates as an exception, which is correct—you cannot recover from running out of memory, only from bad input.
174
+
175
+ ## Construction
176
+
177
+ Streams can be created from constants, collections, resources, and pull-based sources:
178
+
179
+ ### Constant Streams
180
+
181
+ The simplest streams are single-element or empty streams.
182
+
183
+ #### `Stream.empty`
184
+
185
+ An empty stream that emits no elements and succeeds immediately:
186
+
187
+ ```scala
188
+ object Stream {
189
+ val empty: Stream[Nothing, Nothing]
190
+ }
191
+ ```
192
+
193
+ The empty stream is useful as a base case in recursive stream builders or as a neutral element when concatenating:
194
+
195
+ ```scala
196
+ import zio.blocks.streams.*
197
+
198
+ val emptyStream = Stream.empty
199
+ // emptyStream: Stream[Nothing, Nothing] = Stream.empty
200
+ val result = emptyStream.runCollect
201
+ // result: Either[Nothing, Chunk[Nothing]] = Right(IndexedSeq())
202
+ // emptyStream contains no elements
203
+ ```
204
+
205
+ #### `Stream.succeed[A]`
206
+
207
+ Wraps a single value of any type. Specialized overloads avoid boxing for primitives:
208
+
209
+ ```scala
210
+ object Stream {
211
+ def succeed[A](a: A): Stream[Nothing, A]
212
+ def succeed(a: Int): Stream[Nothing, Int]
213
+ def succeed(a: Long): Stream[Nothing, Long]
214
+ def succeed(a: Double): Stream[Nothing, Double]
215
+ // ... and Byte, Short, Char, Float, Boolean variants
216
+ }
217
+ ```
218
+
219
+ When you call `Stream.succeed(value)`, the stream emits exactly one element and completes successfully. This is useful for wrapping a computed value into the stream abstraction:
220
+
221
+ ```scala
222
+ import zio.blocks.streams.*
223
+
224
+ val singleElement = Stream.succeed(42)
225
+ // singleElement: Stream[Nothing, Int] = Stream.succeed(...)
226
+ val result = singleElement.runCollect
227
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(42))
228
+ ```
229
+
230
+ #### `Stream.fail[E]`
231
+
232
+ Creates a stream that fails immediately with a typed error:
233
+
234
+ ```scala
235
+ object Stream {
236
+ def fail[E](error: E): Stream[E, Nothing]
237
+ }
238
+ ```
239
+
240
+ Use `fail` when you need to short-circuit a stream with a known error:
241
+
242
+ ```scala
243
+ import zio.blocks.streams.*
244
+
245
+ sealed trait ApiError
246
+ case class NotFound(id: String) extends ApiError
247
+
248
+ val failedStream = Stream.fail(NotFound("user-123"))
249
+ // failedStream: Stream[NotFound, Nothing] = Stream.fail(...)
250
+ val result = failedStream.runDrain
251
+ // result: Either[NotFound, Unit] = Left(NotFound("user-123"))
252
+ // result is Left(NotFound("user-123"))
253
+ ```
254
+
255
+ #### `Stream.die`
256
+
257
+ Throws an untyped defect (exception) immediately:
258
+
259
+ ```scala
260
+ object Stream {
261
+ def die(t: Throwable): Stream[Nothing, Nothing]
262
+ }
263
+ ```
264
+
265
+ Use `die` for truly exceptional, unrecoverable conditions that should not be caught as typed errors:
266
+
267
+ ```scala
268
+ import zio.blocks.streams.*
269
+
270
+ val dieStream = Stream.die(new Exception("System failure"))
271
+ // dieStream: Stream[Nothing, Nothing] = Stream.die(...)
272
+ ```
273
+
274
+ ### From Collections
275
+
276
+ Streams can be created from existing collections and iterables, making it easy to convert `List`, `Array`, `Chunk`, or custom iterables into lazy streams:
277
+
278
+ #### `Stream.apply[A]`
279
+
280
+ Wraps a variable number of arguments into a stream:
281
+
282
+ ```scala
283
+ object Stream {
284
+ def apply[A](as: A*): Stream[Nothing, A]
285
+ }
286
+ ```
287
+
288
+ This is the most natural way to lift a list of values:
289
+
290
+ ```scala
291
+ import zio.blocks.streams.*
292
+
293
+ val numbers = Stream(1, 2, 3, 4, 5)
294
+ // numbers: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5)
295
+ val result = numbers.runCollect
296
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3, 4, 5))
297
+ ```
298
+
299
+ #### `Stream.fromChunk[A]`
300
+
301
+ Converts a `Chunk` into a stream. Chunks are immutable, indexed sequences optimized for high-performance operations:
302
+
303
+ ```scala
304
+ object Stream {
305
+ def fromChunk[A](chunk: Chunk[A]): Stream[Nothing, A]
306
+ }
307
+ ```
308
+
309
+ Use this when you already have a `Chunk`:
310
+
311
+ ```scala
312
+ import zio.blocks.streams.*
313
+ import zio.blocks.chunk.Chunk
314
+
315
+ val chunk = Chunk(10, 20, 30)
316
+ // chunk: Chunk[Int] = IndexedSeq(10, 20, 30)
317
+ val stream = Stream.fromChunk(chunk)
318
+ // stream: Stream[Nothing, Int] = Stream.fromChunk(...)
319
+ val result = stream.runCollect
320
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(10, 20, 30))
321
+ ```
322
+
323
+ #### `Stream.fromIterable[A]`
324
+
325
+ Converts any `Iterable[A]` (List, Set, Vector, etc.) into a stream:
326
+
327
+ ```scala
328
+ object Stream {
329
+ def fromIterable[A](it: Iterable[A]): Stream[Nothing, A]
330
+ }
331
+ ```
332
+
333
+ This is useful when integrating with legacy Scala collections:
334
+
335
+ ```scala
336
+ import zio.blocks.streams.*
337
+
338
+ val list = List("a", "b", "c")
339
+ // list: List[String] = List("a", "b", "c")
340
+ val stream = Stream.fromIterable(list)
341
+ // stream: Stream[Nothing, String] = Stream.fromIterable(...)
342
+ val result = stream.runCollect
343
+ // result: Either[Nothing, Chunk[String]] = Right(IndexedSeq("a", "b", "c"))
344
+ ```
345
+
346
+ #### `Stream.fromIterator[A]`
347
+
348
+ Converts an `Iterator[A]` into a stream. The iterator is consumed lazily:
349
+
350
+ ```scala
351
+ object Stream {
352
+ def fromIterator[A](it: => Iterator[A]): Stream[Nothing, A]
353
+ }
354
+ ```
355
+
356
+ Create a stream from an iterator and collect all elements:
357
+
358
+ ```scala
359
+ import zio.blocks.streams.*
360
+
361
+ val iter = Iterator(10, 20, 30, 40)
362
+ // iter: Iterator[Int] = empty iterator
363
+ val stream = Stream.fromIterator(iter)
364
+ // stream: Stream[Nothing, Int] = Stream.fromIterator(...)
365
+ val result = stream.runCollect
366
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(10, 20, 30, 40))
367
+ ```
368
+
369
+ ### From Ranges
370
+
371
+ Streams can be created from numeric ranges, providing an efficient way to generate sequences of integers without allocating memory upfront:
372
+
373
+ #### `Stream.range`
374
+
375
+ Emits integers from `from` (inclusive) to `until` (exclusive):
376
+
377
+ ```scala
378
+ object Stream {
379
+ def range(from: Int, until: Int): Stream[Nothing, Int]
380
+ }
381
+ ```
382
+
383
+ This is memory-efficient (does not allocate intermediate collections):
384
+
385
+ ```scala
386
+ import zio.blocks.streams.*
387
+
388
+ val nums = Stream.range(0, 5)
389
+ // nums: Stream[Nothing, Int] = Stream.range(0, 5)
390
+ val result = nums.runCollect
391
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(0, 1, 2, 3, 4))
392
+ ```
393
+
394
+ #### `Stream.fromRange`
395
+
396
+ Converts a Scala `Range` object:
397
+
398
+ ```scala
399
+ object Stream {
400
+ def fromRange(range: Range): Stream[Nothing, Int]
401
+ }
402
+ ```
403
+
404
+ Create a stream from a `Range` and collect elements:
405
+
406
+ ```scala
407
+ import zio.blocks.streams.*
408
+
409
+ val range = 1 to 10 by 2
410
+ // range: Range = Range(1, 3, 5, 7, 9)
411
+ val stream = Stream.fromRange(range)
412
+ // stream: Stream[Nothing, Int] = Stream.fromRange(...)
413
+ val result = stream.runCollect
414
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 3, 5, 7, 9))
415
+ ```
416
+
417
+ ### Generators
418
+
419
+ These constructors create streams from functions and logic, useful for synthesizing infinite or computed sequences:
420
+
421
+ #### `Stream.repeat[A]`
422
+
423
+ Emits the same value infinitely:
424
+
425
+ ```scala
426
+ object Stream {
427
+ def repeat[A](a: A): Stream[Nothing, A]
428
+ }
429
+ ```
430
+
431
+ Infinite streams are safe because streams are lazy; nothing runs until you call a terminal operation with a stopping condition (like `take`):
432
+
433
+ ```scala
434
+ import zio.blocks.streams.*
435
+
436
+ val infinite = Stream.repeat(42)
437
+ // infinite: Stream[Nothing, Int] = Stream.repeat(...)
438
+ val first5 = infinite.take(5)
439
+ // first5: Stream[Nothing, Int] = Stream.repeat(...).take(5)
440
+ val result = first5.runCollect
441
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(42, 42, 42, 42, 42))
442
+ ```
443
+
444
+ #### `Stream.unfold[S, A]`
445
+
446
+ A stateful generator that emits elements based on a fold-like transition function:
447
+
448
+ ```scala
449
+ object Stream {
450
+ def unfold[S, A](s: S)(f: S => Option[(A, S)]): Stream[Nothing, A]
451
+ }
452
+ ```
453
+
454
+ Each iteration, `f` receives the current state and returns either `None` (stop) or `Some((element, nextState))`. This is useful for generating Fibonacci numbers or other sequences defined by a recurrence relation:
455
+
456
+ ```scala
457
+ import zio.blocks.streams.*
458
+
459
+ val fibonacci = Stream.unfold((0, 1)) {
460
+ case (a, b) => Some((a, (b, a + b)))
461
+ }
462
+ // fibonacci: Stream[Nothing, Int] = Stream.unfold(...)
463
+ val first10 = fibonacci.take(10)
464
+ // first10: Stream[Nothing, Int] = Stream.unfold(...).take(10)
465
+ val result = first10.runCollect
466
+ // result: Either[Nothing, Chunk[Int]] = Right(
467
+ // IndexedSeq(0, 1, 1, 2, 3, 5, 8, 13, 21, 34)
468
+ // )
469
+ ```
470
+
471
+ ### Side Effects
472
+
473
+ These constructors embed effects and deferred computation into streams, running actions at stream execution time:
474
+
475
+ #### `Stream.eval[A]`
476
+
477
+ Runs an arbitrary side effect and emits nothing:
478
+
479
+ ```scala
480
+ object Stream {
481
+ def eval(f: => Any): Stream[Nothing, Nothing]
482
+ }
483
+ ```
484
+
485
+ Use `eval` when you want a side effect in a stream (e.g., logging, metrics) but no element:
486
+
487
+ ```scala
488
+ import zio.blocks.streams.*
489
+
490
+ val sideEffect = Stream.eval(println("Executing side effect"))
491
+ // sideEffect: Stream[Nothing, Nothing] = Stream.suspend(...)
492
+ val result = sideEffect.runDrain
493
+ // Executing side effect
494
+ // result: Either[Nothing, Unit] = Right(())
495
+ ```
496
+
497
+ #### `Stream.attempt[A]`
498
+
499
+ Wraps a potentially throwing computation, converting non-fatal `Throwable`s into a typed error. Fatal errors (like `OutOfMemoryError`) are not caught and propagate as exceptions:
500
+
501
+ ```scala
502
+ object Stream {
503
+ def attempt[A](f: => A): Stream[Throwable, A]
504
+ }
505
+ ```
506
+
507
+ Use `attempt` when you have legacy code that throws exceptions:
508
+
509
+ ```scala
510
+ import zio.blocks.streams.*
511
+
512
+ def unsafeJsonParse(s: String): Int = s.toInt
513
+
514
+ val parsed = Stream.attempt(unsafeJsonParse("42"))
515
+ // parsed: Stream[Throwable, Int] = Stream.suspend(...)
516
+ val result = parsed.runCollect
517
+ // result: Either[Throwable, Chunk[Int]] = Right(IndexedSeq(42))
518
+ ```
519
+
520
+ #### `Stream.attemptEval`
521
+
522
+ Evaluates a side effect and converts any thrown exception into a typed `Throwable` error. Unlike `eval`, this captures exceptions and emits nothing:
523
+
524
+ ```scala
525
+ object Stream {
526
+ def attemptEval(f: => Any): Stream[Throwable, Nothing]
527
+ }
528
+ ```
529
+
530
+ Use `attemptEval` when you need to safely execute an effect that might throw, but you don't need to emit any elements:
531
+
532
+ ```scala
533
+ import zio.blocks.streams.*
534
+
535
+ val effect = Stream.attemptEval {
536
+ val file = new java.io.File("nonexistent.txt")
537
+ if (!file.exists()) throw new java.io.FileNotFoundException("File not found")
538
+ }
539
+ val result = effect.runDrain
540
+ ```
541
+
542
+ #### `Stream.defer[A]`
543
+
544
+ Defers the execution of a side effect until the stream is run:
545
+
546
+ ```scala
547
+ object Stream {
548
+ def defer(f: => Unit): Stream[Nothing, Nothing]
549
+ }
550
+ ```
551
+
552
+ Defer side effects until the stream executes:
553
+
554
+ ```scala
555
+ import zio.blocks.streams.*
556
+
557
+ val deferred = Stream.defer(println("Effect runs when stream executes"))
558
+ // deferred: Stream[Nothing, Nothing] = Stream.defer(...)
559
+ val result = deferred.runDrain
560
+ // Effect runs when stream executes
561
+ // result: Either[Nothing, Unit] = Right(())
562
+ ```
563
+
564
+ #### `Stream.suspend[E, A]`
565
+
566
+ Defers the creation of a stream until run time, useful for recursive stream definitions:
567
+
568
+ ```scala
569
+ object Stream {
570
+ def suspend[E, A](stream: => Stream[E, A]): Stream[E, A]
571
+ }
572
+ ```
573
+
574
+ Define a recursive stream safely:
575
+
576
+ ```scala
577
+ import zio.blocks.streams.*
578
+
579
+ def countDown(n: Int): Stream[Nothing, Int] =
580
+ if (n <= 0) Stream.empty
581
+ else Stream.suspend(Stream.succeed(n) ++ countDown(n - 1))
582
+
583
+ val result = countDown(5).runCollect
584
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(5, 4, 3, 2, 1))
585
+ ```
586
+
587
+ ### I/O
588
+
589
+ Streams can read from external I/O sources like files and readers, automatically managing resource cleanup:
590
+
591
+ #### `Stream.fromInputStream`
592
+
593
+ Reads bytes from a Java `InputStream`, managing the resource:
594
+
595
+ ```scala
596
+ object Stream {
597
+ def fromInputStream(is: java.io.InputStream): Stream[java.io.IOException, Int]
598
+ }
599
+ ```
600
+
601
+ The stream automatically closes the input stream when done:
602
+
603
+ ```scala
604
+ import zio.blocks.streams.*
605
+ import java.io.ByteArrayInputStream
606
+
607
+ val data = new ByteArrayInputStream("Hello".getBytes)
608
+ // data: ByteArrayInputStream = java.io.ByteArrayInputStream@6a864582
609
+ val bytes = Stream.fromInputStream(data)
610
+ // bytes: Stream[IOException, Byte] = Stream.fromAcquireRelease(...)
611
+ val result = bytes.runCollect
612
+ // result: Either[IOException, Chunk[Byte]] = Right(
613
+ // IndexedSeq(72, 101, 108, 108, 111)
614
+ // )
615
+ ```
616
+
617
+ #### `Stream.fromJavaReader`
618
+
619
+ Reads characters from a Java `Reader`:
620
+
621
+ ```scala
622
+ object Stream {
623
+ def fromJavaReader(r: java.io.Reader): Stream[java.io.IOException, Char]
624
+ }
625
+ ```
626
+
627
+ Read characters from a string reader:
628
+
629
+ ```scala
630
+ import zio.blocks.streams.*
631
+ import java.io.StringReader
632
+
633
+ val reader = new StringReader("hello world")
634
+ // reader: StringReader = java.io.StringReader@793e494
635
+ val stream = Stream.fromJavaReader(reader)
636
+ // stream: Stream[IOException, Char] = Stream.fromAcquireRelease(...)
637
+ val result = stream.runCollect
638
+ // result: Either[IOException, Chunk[Char]] = Right(
639
+ // IndexedSeq('h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd')
640
+ // )
641
+ ```
642
+
643
+ #### `Stream.fromInputStreamUnmanaged`
644
+
645
+ Reads bytes from a Java `InputStream` without automatic resource management. The caller is responsible for closing the stream:
646
+
647
+ ```scala
648
+ object Stream {
649
+ def fromInputStreamUnmanaged(is: java.io.InputStream): Stream[java.io.IOException, Int]
650
+ }
651
+ ```
652
+
653
+ Use this when you need to manage the stream's lifecycle yourself, for example when the stream is created from a long-lived resource:
654
+
655
+ ```scala
656
+ import zio.blocks.streams.*
657
+ import java.io.ByteArrayInputStream
658
+
659
+ val data = new ByteArrayInputStream("Data".getBytes)
660
+ val bytes = Stream.fromInputStreamUnmanaged(data)
661
+ val result = bytes.runCollect
662
+ // Caller must close data when done
663
+ ```
664
+
665
+ #### `Stream.fromJavaReaderUnmanaged`
666
+
667
+ Reads characters from a Java `Reader` without automatic resource management. The caller is responsible for closing the reader:
668
+
669
+ ```scala
670
+ object Stream {
671
+ def fromJavaReaderUnmanaged(r: java.io.Reader): Stream[java.io.IOException, Char]
672
+ }
673
+ ```
674
+
675
+ Use this when you need to manage the reader's lifecycle yourself:
676
+
677
+ ```scala
678
+ import zio.blocks.streams.*
679
+ import java.io.StringReader
680
+
681
+ val reader = new StringReader("managed externally")
682
+ val stream = Stream.fromJavaReaderUnmanaged(reader)
683
+ val result = stream.runCollect
684
+ // Caller must close reader when done
685
+ ```
686
+
687
+ ## Transformations
688
+
689
+ Streams provide powerful operations for transforming elements, flattening nested structures, filtering, and managing state:
690
+
691
+ ### Element-wise Transformations
692
+
693
+ These operations apply functions to stream elements one-by-one, applying the transformation lazily as elements are pulled:
694
+
695
+ #### `Stream#map[B]`
696
+
697
+ Applies a function to each element:
698
+
699
+ ```scala
700
+ trait Stream[+E, +A] {
701
+ def map[B](f: A => B): Stream[E, B]
702
+ }
703
+ ```
704
+
705
+ `map` does not run immediately; it builds up a description of the transformation. Only when you call a terminal operation does the mapping happen:
706
+
707
+ ```scala
708
+ import zio.blocks.streams.*
709
+
710
+ val nums = Stream(1, 2, 3)
711
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3)
712
+ val doubled = nums.map(_ * 2)
713
+ // doubled: Stream[Nothing, Int] = Stream(1, 2, 3).map(...)
714
+ val result = doubled.runCollect
715
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(2, 4, 6))
716
+ ```
717
+
718
+ **Key point:** `Stream#map` is covariant in the output type because it preserves the error type and only transforms elements. The implicit `JvmType.Infer[A]` and `JvmType.Infer[B]` enable compile-time dispatch to unboxed fast paths for primitive types (Int, Long, Double, etc.).
719
+
720
+ #### `Stream#mapError[E2]`
721
+
722
+ Transforms typed errors without affecting elements:
723
+
724
+ ```scala
725
+ trait Stream[+E, +A] {
726
+ inline def mapError[E2](f: E => E2): Stream[E2, A]
727
+ }
728
+ ```
729
+
730
+ Use `mapError` to convert one error type to another:
731
+
732
+ ```scala
733
+ import zio.blocks.streams.*
734
+
735
+ sealed trait ApiError
736
+ case class ServerError(msg: String) extends ApiError
737
+ case class NetworkError() extends ApiError
738
+
739
+ val mayFail: Stream[NetworkError, String] = Stream.fail(NetworkError())
740
+ val mapped = mayFail.mapError(e => ServerError("Connection failed"))
741
+ ```
742
+
743
+ #### `Stream#filter`
744
+
745
+ Emits only elements that satisfy a predicate:
746
+
747
+ ```scala
748
+ trait Stream[+E, +A] {
749
+ def filter(pred: A => Boolean): Stream[E, A]
750
+ }
751
+ ```
752
+
753
+ Short-circuits: as soon as the sink says "stop," filtering stops:
754
+
755
+ ```scala
756
+ import zio.blocks.streams.Stream
757
+
758
+ val nums = Stream(1, 2, 3, 4, 5)
759
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5)
760
+ val evens = nums.filter(_ % 2 == 0)
761
+ // evens: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5).filter(...)
762
+ val result = evens.runCollect
763
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(2, 4))
764
+ ```
765
+
766
+ #### `Stream#collect[B]`
767
+
768
+ Applies a partial function, emitting only defined results:
769
+
770
+ ```scala
771
+ trait Stream[+E, +A] {
772
+ def collect[B](pf: PartialFunction[A, B]): Stream[E, B]
773
+ }
774
+ ```
775
+
776
+ This combines filtering and mapping in one step:
777
+
778
+ ```scala
779
+ import zio.blocks.streams.*
780
+
781
+ val mixed = Stream(1, "a", 2, "b", 3)
782
+ // mixed: Stream[Nothing, Int | String] = Stream(1, a, 2, b, 3)
783
+ val numbers = mixed.collect { case n: Int => n }
784
+ // numbers: Stream[Nothing, Int] = Stream(1, a, 2, b, 3).collect(...)
785
+ val result = numbers.runCollect
786
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3))
787
+ ```
788
+
789
+ ### Stateful Transformations
790
+
791
+ These operations maintain internal state while processing elements, allowing you to fold computations into the transformation:
792
+
793
+ #### `Stream#mapAccum[S, B]`
794
+
795
+ Maintains state while transforming each element:
796
+
797
+ ```scala
798
+ trait Stream[+E, +A] {
799
+ def mapAccum[S, B](init: S)(f: (S, A) => (S, B)): Stream[E, B]
800
+ }
801
+ ```
802
+
803
+ `mapAccum` threads a state value through the transformation. At each step, you receive the current state and the element, return a new state and output element:
804
+
805
+ ```scala
806
+ import zio.blocks.streams.*
807
+
808
+ val nums = Stream(1, 2, 3)
809
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3)
810
+ val indexed = nums.mapAccum(0)((idx, x) => (idx + 1, (idx, x)))
811
+ // indexed: Stream[Nothing, Tuple2[Int, Int]] = Stream(1, 2, 3).mapAccum(...)
812
+ val result = indexed.runCollect
813
+ // result: Either[Nothing, Chunk[Tuple2[Int, Int]]] = Right(
814
+ // IndexedSeq((0, 1), (1, 2), (2, 3))
815
+ // )
816
+ ```
817
+
818
+ #### `Stream#scan[S]`
819
+
820
+ Like `mapAccum`, but also emits the state at each step (not the mapped value):
821
+
822
+ ```scala
823
+ trait Stream[+E, +A] {
824
+ def scan[S](init: S)(f: (S, A) => S): Stream[E, S]
825
+ }
826
+ ```
827
+
828
+ This is useful for computing running totals, moving averages, or other cumulative statistics:
829
+
830
+ ```scala
831
+ import zio.blocks.streams.*
832
+
833
+ val nums = Stream(1, 2, 3, 4)
834
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4)
835
+ val cumsum = nums.scan(0)(_ + _)
836
+ // cumsum: Stream[Nothing, Int] = Stream(1, 2, 3, 4).scan(...)
837
+ val result = cumsum.runCollect
838
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(0, 1, 3, 6, 10))
839
+ ```
840
+
841
+ ### Flat-Mapping (Nested Streams)
842
+
843
+ `flatMap[E2, B]` — Maps each element to a stream and flattens the results.:
844
+
845
+ ```scala
846
+ trait Stream[+E, +A] {
847
+ def flatMap[E2, B](f: A => Stream[E2, B]): Stream[E | E2, B]
848
+ }
849
+ ```
850
+
851
+ `Stream#flatMap` is sequential: streams are processed one at a time, in order. This is essential for resource safety: if each inner stream acquires a resource, `Stream#flatMap` ensures they are released in proper FIFO order:
852
+
853
+ ```scala
854
+ import zio.blocks.streams.*
855
+
856
+ val ids = Stream(1, 2, 3)
857
+ // ids: Stream[Nothing, Int] = Stream(1, 2, 3)
858
+ val expanded = ids.flatMap(id => Stream(s"${id}-a", s"${id}-b"))
859
+ // expanded: Stream[Nothing, String] = Stream(1, 2, 3).flatMap(...)
860
+ val result = expanded.runCollect
861
+ // result: Either[Nothing, Chunk[String]] = Right(
862
+ // IndexedSeq("1-a", "1-b", "2-a", "2-b", "3-a", "3-b")
863
+ // )
864
+ ```
865
+
866
+ #### `Stream.flattenAll[E, A]`
867
+
868
+ Flattens a stream of streams into a single stream, processing them sequentially:
869
+
870
+ ```scala
871
+ object Stream {
872
+ def flattenAll[E, A](streams: Stream[E, Stream[E, A]]): Stream[E, A]
873
+ }
874
+ ```
875
+
876
+ This is equivalent to `flatMap(identity)`. Use `flattenAll` when you already have a stream of streams and want to flatten it without applying a transformation:
877
+
878
+ ```scala
879
+ import zio.blocks.streams.*
880
+
881
+ val nested = Stream.fromIterable(List(
882
+ Stream(1, 2),
883
+ Stream(3, 4)
884
+ ))
885
+ // nested: Stream[Nothing, Stream[Nothing, Int]] = Stream.fromIterable(...)
886
+ val flat = Stream.flattenAll(nested)
887
+ // flat: Stream[Nothing, Int] = Stream.fromIterable(...).flatMap(...)
888
+ val result = flat.runCollect
889
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3, 4))
890
+ ```
891
+
892
+ ## Windowing
893
+
894
+ Streams can be grouped, sliced, and scanned to process data in temporal windows. These operations group elements into chunks and slide windows over the stream for batch processing:
895
+
896
+ ### `Stream#grouped[A]`
897
+
898
+ Collects elements into fixed-size chunks:
899
+
900
+ ```scala
901
+ trait Stream[+E, +A] {
902
+ def grouped(n: Int): Stream[E, Chunk[A]]
903
+ }
904
+ ```
905
+
906
+ The last chunk may contain fewer than `n` elements:
907
+
908
+ ```scala
909
+ import zio.blocks.streams.*
910
+
911
+ val nums = Stream(1, 2, 3, 4, 5)
912
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5)
913
+ val groups = nums.grouped(2)
914
+ // groups: Stream[Nothing, Chunk[Int]] = Stream(1, 2, 3, 4, 5).chunked(2)
915
+ val result = groups.runCollect
916
+ // result: Either[Nothing, Chunk[Chunk[Int]]] = Right(
917
+ // IndexedSeq(IndexedSeq(1, 2), IndexedSeq(3, 4), IndexedSeq(5))
918
+ // )
919
+ ```
920
+
921
+ ### `Stream#sliding[A]`
922
+
923
+ Creates a sliding window of size `n`, optionally stepping by `step` elements:
924
+
925
+ ```scala
926
+ trait Stream[+E, +A] {
927
+ def sliding(n: Int, step: Int = 1): Stream[E, Chunk[A]]
928
+ }
929
+ ```
930
+
931
+ This is useful for computing local statistics or detecting patterns in sequences:
932
+
933
+ ```scala
934
+ import zio.blocks.streams.*
935
+
936
+ val nums = Stream(1, 2, 3, 4, 5)
937
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5)
938
+ val windows = nums.sliding(3, step = 1)
939
+ // windows: Stream[Nothing, Chunk[Int]] = Stream(1, 2, 3, 4, 5).sliding(3, 1)
940
+ val result = windows.runCollect
941
+ // result: Either[Nothing, Chunk[Chunk[Int]]] = Right(
942
+ // IndexedSeq(IndexedSeq(1, 2, 3), IndexedSeq(2, 3, 4), IndexedSeq(3, 4, 5))
943
+ // )
944
+ ```
945
+
946
+ ## Combining Streams
947
+
948
+ Streams can be sequentially concatenated, zipped together, or merged:
949
+
950
+ ### Sequential Concatenation
951
+
952
+ `++[E2, A2]` or `concat[E2, A2]` — Emits all elements of the first stream, then all elements of the second stream:
953
+
954
+ ```scala
955
+ trait Stream[+E, +A] {
956
+ def ++[E2, A2](that: Stream[E2, A2]): Stream[E | E2, A | A2] = concat(that)
957
+ }
958
+ ```
959
+
960
+ The result type follows the same widening rules as Scala 3 unions:
961
+
962
+ - identical types stay unchanged (`A ++ A => A`)
963
+ - subtypes widen to the supertype (`Dog ++ Animal => Animal`)
964
+ - siblings with a common meaningful supertype widen to that supertype (`Dog ++ Cat => Animal`, when both extend a sealed `Animal`)
965
+ - otherwise the result is a disjoint union (`String ++ Int => String | Int`)
966
+
967
+ On Scala 3, disjoint concat results are native unions. On Scala 2, the same/subtype and sibling cases collapse to the wider existing type (zero-cost, values are reused as-is); only types without a shared meaningful supertype fall back to `Either[L, R]`.
968
+
969
+ Evaluation is sequential: the second stream only starts when the first completes:
970
+
971
+ ```scala
972
+ import zio.blocks.streams.*
973
+
974
+ val first = Stream(1, 2)
975
+ // first: Stream[Nothing, Int] = Stream(1, 2)
976
+ val second = Stream(3, 4)
977
+ // second: Stream[Nothing, Int] = Stream(3, 4)
978
+ val combined = first ++ second
979
+ // combined: Stream[Nothing, Int] = Stream(1, 2) ++ Stream(3, 4)
980
+ val result = combined.runCollect
981
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3, 4))
982
+ ```
983
+
984
+ For unrelated element types, Scala 3 produces a direct union while Scala 2 produces `Either`:
985
+
986
+ <Tabs groupId="scala-version" defaultValue="scala2">
987
+ <TabItem value="scala2" label="Scala 2.13">
988
+
989
+ ```scala
990
+ val combined: Stream[Nothing, Either[String, Int]] =
991
+ Stream.succeed("left") ++ Stream.succeed(1)
992
+ ```
993
+
994
+ </TabItem>
995
+ <TabItem value="scala3" label="Scala 3.x">
996
+
997
+ ```scala
998
+ val combined: Stream[Nothing, String | Int] =
999
+ Stream.succeed("left") ++ Stream.succeed(1)
1000
+ ```
1001
+
1002
+ </TabItem>
1003
+ </Tabs>
1004
+
1005
+ ```scala
1006
+ import zio.blocks.streams.*
1007
+ import zio.blocks.chunk.Chunk
1008
+
1009
+ val concatResult = (Stream.succeed("left") ++ Stream.succeed(1)).runCollect
1010
+ // concatResult: Either[Nothing, Chunk[String | Int]] = Right(
1011
+ // IndexedSeq("left", 1)
1012
+ // )
1013
+
1014
+ assert(concatResult == Right(Chunk[String | Int]("left", 1)))
1015
+ ```
1016
+
1017
+ The error channel follows the same rules. Same/subtype errors collapse; unrelated errors remain disjoint:
1018
+
1019
+ ```scala
1020
+ import zio.blocks.streams.*
1021
+
1022
+ sealed trait LeftError
1023
+ case class Boom(msg: String) extends LeftError
1024
+ case class Missing(code: Int)
1025
+
1026
+ val left: Stream[LeftError, String] = Stream.fail(Boom("boom"))
1027
+ // left: Stream[LeftError, String] = Stream.fail(...)
1028
+ val right = Stream.succeed(true)
1029
+ // right: Stream[Nothing, Boolean] = Stream.succeed(...)
1030
+
1031
+ left.runCollect
1032
+ // res37: Either[LeftError, Chunk[String]] = Left(Boom("boom"))
1033
+
1034
+ val failed = left ++ (Stream.fail(Missing(404)): Stream[Missing, Boolean])
1035
+ // failed: Stream[LeftError | Missing, String | Boolean] = Stream.fail(...) ++ Stream.fail(...)
1036
+ failed.runCollect
1037
+ // res38: Either[LeftError | Missing, Chunk[String | Boolean]] = Left(
1038
+ // Boom("boom")
1039
+ // )
1040
+ ```
1041
+
1042
+ There is no separate `choice` operator anymore. Use `++` / `concat` for all sequential combination; the result type already reflects the Scala 3-style union semantics.
1043
+
1044
+ ### Zipping
1045
+
1046
+ Zips two streams together as tuples (an extension method, not an instance method):
1047
+
1048
+ ```scala
1049
+ extension [E, A](stream: Stream[E, A])
1050
+ def &&[E2, B, C](that: Stream[E2, B])(
1051
+ using Tuples[A, B] { Out = C }
1052
+ ): Stream[E | E2, C]
1053
+ ```
1054
+
1055
+ The result streams have the same length as the shorter input:
1056
+
1057
+ ```scala
1058
+ import zio.blocks.streams.*
1059
+
1060
+ val nums = Stream(1, 2, 3)
1061
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3)
1062
+ val chars = Stream('a', 'b')
1063
+ // chars: Stream[Nothing, Char] = Stream(a, b)
1064
+ val zipped = nums && chars
1065
+ // zipped: Stream[Nothing, Tuple2[Int, Char]] = Stream.fromReader(...)
1066
+ val result = zipped.runCollect
1067
+ // result: Either[Nothing, Chunk[Tuple2[Int, Char]]] = Right(
1068
+ // IndexedSeq((1, 'a'), (2, 'b'))
1069
+ // )
1070
+ ```
1071
+
1072
+ ## Other Operations
1073
+
1074
+ Common utilities for deduplication, draining, and error recovery:
1075
+
1076
+ ### Filtering Duplicates
1077
+
1078
+ These operations remove duplicate elements, useful for deduplicating streams before processing:
1079
+
1080
+ #### `Stream#distinct[A]`
1081
+
1082
+ Emits only unique elements (using a mutable `HashSet` internally):
1083
+
1084
+ ```scala
1085
+ trait Stream[+E, +A] {
1086
+ def distinct(implicit jtA: JvmType.Infer[A]): Stream[E, A]
1087
+ }
1088
+ ```
1089
+
1090
+ This consumes memory proportional to the number of unique elements:
1091
+
1092
+ ```scala
1093
+ import zio.blocks.streams.*
1094
+
1095
+ val nums = Stream(1, 2, 2, 3, 3, 3)
1096
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 2, 3, 3, ...)
1097
+ val unique = nums.distinct
1098
+ // unique: Stream[Nothing, Int] = Stream(1, 2, 2, 3, 3, ...).distinct
1099
+ val result = unique.runCollect
1100
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3))
1101
+ ```
1102
+
1103
+ #### `Stream#distinctBy[K]`
1104
+
1105
+ Emits only elements whose key (computed by `f`) has not been seen before:
1106
+
1107
+ ```scala
1108
+ trait Stream[+E, +A] {
1109
+ def distinctBy[K](f: A => K)(implicit jtA: JvmType.Infer[A]): Stream[E, A]
1110
+ }
1111
+ ```
1112
+
1113
+ This deduplicates elements by a computed key, keeping only the first occurrence of each key:
1114
+
1115
+ ```scala
1116
+ import zio.blocks.streams.*
1117
+
1118
+ case class Person(id: Int, name: String)
1119
+
1120
+ val people = Stream(
1121
+ Person(1, "Alice"),
1122
+ Person(2, "Bob"),
1123
+ Person(1, "Alice2"), // same id as first, dropped
1124
+ Person(3, "Charlie")
1125
+ )
1126
+
1127
+ val unique = people.distinctBy(_.id)
1128
+ val result = unique.runCollect
1129
+ ```
1130
+
1131
+ ### Skipping and Taking
1132
+
1133
+ These operations skip or limit elements, allowing you to keep or drop unwanted portions of the stream:
1134
+
1135
+ #### `Stream#drop`
1136
+
1137
+ Skips the first `n` elements:
1138
+
1139
+ ```scala
1140
+ trait Stream[+E, +A] {
1141
+ def drop(n: Long): Stream[E, A]
1142
+ }
1143
+ ```
1144
+
1145
+ Dropping the first 3 elements and collecting the remainder:
1146
+
1147
+ ```scala
1148
+ import zio.blocks.streams.*
1149
+
1150
+ val nums = Stream(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
1151
+ val remaining = nums.drop(3)
1152
+ val result = remaining.runCollect
1153
+ ```
1154
+
1155
+ #### `Stream#take`
1156
+
1157
+ Emits at most the first `n` elements, then stops:
1158
+
1159
+ ```scala
1160
+ trait Stream[+E, +A] {
1161
+ def take(n: Long): Stream[E, A]
1162
+ }
1163
+ ```
1164
+
1165
+ This naturally short-circuits: the stream stops pulling from upstream:
1166
+
1167
+ ```scala
1168
+ import zio.blocks.streams.*
1169
+
1170
+ val nums = Stream.range(0, 1000)
1171
+ // nums: Stream[Nothing, Int] = Stream.range(0, 1000)
1172
+ val first10 = nums.take(10)
1173
+ // first10: Stream[Nothing, Int] = Stream.range(0, 1000).take(10)
1174
+ val result = first10.runCollect
1175
+ // result: Either[Nothing, Chunk[Int]] = Right(
1176
+ // IndexedSeq(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
1177
+ // )
1178
+ ```
1179
+
1180
+ #### `Stream#takeWhile`
1181
+
1182
+ Emits elements while a predicate is true, then stops:
1183
+
1184
+ ```scala
1185
+ trait Stream[+E, +A] {
1186
+ def takeWhile(pred: A => Boolean): Stream[E, A]
1187
+ }
1188
+ ```
1189
+
1190
+ Taking elements while they are less than 6 stops early without processing the rest:
1191
+
1192
+ ```scala
1193
+ import zio.blocks.streams.*
1194
+
1195
+ val nums = Stream(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
1196
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5, ...)
1197
+ val firstFive = nums.takeWhile(_ < 6)
1198
+ // firstFive: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5, ...).takeWhile(...)
1199
+ val result = firstFive.runCollect
1200
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3, 4, 5))
1201
+ ```
1202
+
1203
+ ### Interspersing
1204
+
1205
+ `intersperse[A1 >: A]` — Inserts a separator value between every two elements.:
1206
+
1207
+ ```scala
1208
+ trait Stream[+E, +A] {
1209
+ def intersperse[A1 >: A](sep: A1): Stream[E, A1]
1210
+ }
1211
+ ```
1212
+
1213
+ This is useful for rendering comma-separated lists or row delimiters:
1214
+
1215
+ ```scala
1216
+ import zio.blocks.streams.*
1217
+
1218
+ val items = Stream("a", "b", "c")
1219
+ // items: Stream[Nothing, String] = Stream(a, b, c)
1220
+ val separated = items.intersperse(", ")
1221
+ // separated: Stream[Nothing, String] = Stream(a, b, c).intersperse(...)
1222
+ val result = separated.runCollect
1223
+ // result: Either[Nothing, Chunk[String]] = Right(
1224
+ // IndexedSeq("a", ", ", "b", ", ", "c")
1225
+ // )
1226
+ ```
1227
+
1228
+ ### Repeating
1229
+
1230
+ `repeated` — Repeats each element once, then emits the entire stream again, repeatedly.:
1231
+
1232
+ ```scala
1233
+ trait Stream[+E, +A] {
1234
+ def repeated: Stream[E, A]
1235
+ }
1236
+ ```
1237
+
1238
+ This creates an infinite repetition of the stream:
1239
+
1240
+ ```scala
1241
+ import zio.blocks.streams.*
1242
+
1243
+ val original = Stream(1, 2)
1244
+ // original: Stream[Nothing, Int] = Stream(1, 2)
1245
+ val repeated = original.repeated.take(6)
1246
+ // repeated: Stream[Nothing, Int] = Stream(1, 2).repeated.take(6)
1247
+ val result = repeated.runCollect
1248
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 1, 2, 1, 2))
1249
+ ```
1250
+
1251
+ ### Side Effects
1252
+
1253
+ `tapEach` — Applies a function to each element for side effects, passing the element through unchanged.:
1254
+
1255
+ ```scala
1256
+ trait Stream[+E, +A] {
1257
+ def tapEach(f: A => Unit)(implicit jtA: JvmType.Infer[A]): Stream[E, A]
1258
+ }
1259
+ ```
1260
+
1261
+ Use `tapEach` for logging or metrics:
1262
+
1263
+ ```scala
1264
+ import zio.blocks.streams.*
1265
+
1266
+ val nums = Stream(1, 2, 3)
1267
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3)
1268
+ val logged = nums.tapEach(x => println(s"Element: $x"))
1269
+ // logged: Stream[Nothing, Int] = Stream(1, 2, 3).map(...)
1270
+ val result = logged.runCollect
1271
+ // Element: 1
1272
+ // Element: 2
1273
+ // Element: 3
1274
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3))
1275
+ ```
1276
+
1277
+ ## Error Handling
1278
+
1279
+ Streams distinguish between recoverable business errors and unexpected exceptions, providing separate recovery mechanisms for each:
1280
+
1281
+ ### Typed Error vs Untyped Defect
1282
+
1283
+ ZIO Blocks distinguishes two error channels:
1284
+
1285
+ - **Typed errors (`E`)**: Recoverable business logic errors. Returned as `Left(e)` from terminal operations.
1286
+ - **Untyped defects (`Throwable`)**: Unexpected exceptions (bugs, system failures). Propagate as thrown exceptions.
1287
+
1288
+ Internally, typed errors are wrapped in `StreamError` (a non-fatal exception) and caught by the terminal operation to surface as `Left(e)`. Untyped `Throwable`s are not caught and propagate upward.
1289
+
1290
+ This separation allows you to:
1291
+ - Use `catchAll` and `orElse` for business logic errors
1292
+ - Use `catchDefect` or try-catch for unexpected exceptions
1293
+ - Avoid accidentally silencing real bugs by catching all errors
1294
+
1295
+ Streams distinguish between recoverable domain errors and fatal defects, with flexible recovery patterns:
1296
+
1297
+ ### Recovering from Typed Errors
1298
+
1299
+ These operations handle typed errors gracefully by recovering with alternative streams:
1300
+
1301
+ #### `Stream#catchAll[E2, A1]`
1302
+
1303
+ Recovers from any typed error by switching to a recovery stream:
1304
+
1305
+ ```scala
1306
+ trait Stream[+E, +A] {
1307
+ def catchAll[E2, A1](f: E => Stream[E2, A1]): Stream[E2, A | A1]
1308
+ }
1309
+ ```
1310
+
1311
+ The recovery function receives the error and can return a new stream:
1312
+
1313
+ ```scala
1314
+ import zio.blocks.streams.*
1315
+
1316
+ sealed trait Error
1317
+ case object NotFound extends Error
1318
+
1319
+ val mayFail: Stream[Error, String] = Stream.fail(NotFound)
1320
+ // mayFail: Stream[Error, String] = Stream.fail(...)
1321
+ val recovered = mayFail.catchAll(_ => Stream.succeed("default"))
1322
+ // recovered: Stream[Nothing, String] = Stream.fail(...).catchAll(...)
1323
+ val result = recovered.runCollect
1324
+ // result: Either[Nothing, Chunk[String]] = Right(IndexedSeq("default"))
1325
+ ```
1326
+
1327
+ #### `Stream#orElse[E2, A1]`
1328
+
1329
+ If this stream fails, tries the fallback stream. The fallback is evaluated lazily, only on error:
1330
+
1331
+ ```scala
1332
+ trait Stream[+E, +A] {
1333
+ def orElse[E2, A1](that: => Stream[E2, A1]): Stream[E2, A | A1]
1334
+ }
1335
+ ```
1336
+
1337
+ `||` is an alias for `orElse`:
1338
+
1339
+ ```scala
1340
+ import zio.blocks.streams._
1341
+
1342
+ val primary = Stream.fail("error")
1343
+ // primary: Stream[String, Nothing] = Stream.fail(...)
1344
+ val fallback = Stream.succeed(42)
1345
+ // fallback: Stream[Nothing, Int] = Stream.succeed(...)
1346
+ val result = (primary || fallback).runCollect
1347
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(42))
1348
+ ```
1349
+
1350
+ ### Recovering from Defects
1351
+
1352
+ `catchDefect[E1, A1]` — Catches untyped defects (exceptions not wrapped as typed errors) using a partial function.:
1353
+
1354
+ ```scala
1355
+ trait Stream[+E, +A] {
1356
+ def catchDefect[E1, A1](
1357
+ f: PartialFunction[Throwable, Stream[E1, A1]]
1358
+ ): Stream[E | E1, A | A1]
1359
+ }
1360
+ ```
1361
+
1362
+ Use `catchDefect` when you need to handle unexpected exceptions that were not wrapped by `attempt`:
1363
+
1364
+ ```scala
1365
+ import zio.blocks.streams.*
1366
+
1367
+ val risky = Stream.die(new IllegalArgumentException("Not allowed"))
1368
+ val safe = risky.catchDefect {
1369
+ case e: IllegalArgumentException => Stream.succeed(-1)
1370
+ }
1371
+ val result = safe.runCollect
1372
+ ```
1373
+
1374
+ ## Resource Management
1375
+
1376
+ **The Problem:** Resources like files, database connections, and network sockets must be explicitly closed after use. If you just process them in a stream and forget to close, you leak resources. If an error occurs during processing, manual cleanup code might be skipped.
1377
+
1378
+ **The Solution:** ZIO Blocks streams provide three patterns for safe, automatic resource cleanup:
1379
+
1380
+ ### `Stream.fromAcquireRelease[R, E, A]`
1381
+
1382
+ Acquires a resource, uses it in a stream, and **guarantees cleanup regardless of success or failure**:
1383
+
1384
+ ```scala
1385
+ object Stream {
1386
+ def fromAcquireRelease[R, E, A](
1387
+ acquire: => R, // How to open the resource
1388
+ release: R => Unit = (r: R) => // How to close it (defaults to .close())
1389
+ r match {
1390
+ case ac: AutoCloseable => ac.close()
1391
+ case _ => ()
1392
+ }
1393
+ )(use: R => Stream[E, A]): Stream[E, A]
1394
+ }
1395
+ ```
1396
+
1397
+ This is the fundamental pattern for safe resource handling:
1398
+ 1. **Acquire** — opens the resource (runs once, before streaming)
1399
+ 2. **Use** — streams elements from the resource
1400
+ 3. **Release** — closes the resource in a `finally` block (always runs, even on error)
1401
+
1402
+ Here's an example with automatic cleanup:
1403
+
1404
+ ```scala
1405
+ import zio.blocks.streams.*
1406
+
1407
+ case class DatabaseConnection(id: String) {
1408
+ def close(): Unit = println(s"Closing connection $id")
1409
+ def query(q: String): List[String] = List("result1", "result2")
1410
+ }
1411
+
1412
+ val managed = Stream.fromAcquireRelease(
1413
+ acquire = {
1414
+ println("Opening database connection")
1415
+ DatabaseConnection("db-1")
1416
+ },
1417
+ release = _.close() // Guaranteed to run even if streaming fails
1418
+ )(conn => Stream.fromIterable(conn.query("SELECT *")))
1419
+
1420
+ val result = managed.runCollect
1421
+ // Output:
1422
+ // Opening database connection
1423
+ // Closing database connection <-- always happens
1424
+ ```
1425
+
1426
+ Even if the stream fails, cleanup runs:
1427
+
1428
+ ```scala
1429
+ import zio.blocks.streams.*
1430
+
1431
+ val managed = Stream.fromAcquireRelease(
1432
+ acquire = { println("Opening"); "resource" },
1433
+ release = { r => println(s"Closing $r") }
1434
+ )(_ => Stream.fail("error occurred"))
1435
+
1436
+ val result = managed.runCollect
1437
+ // Output:
1438
+ // Opening
1439
+ // Closing resource <-- cleanup still runs even with error
1440
+ // result: Either[String, Chunk[Nothing]] = Left("error occurred")
1441
+ ```
1442
+
1443
+ ### `Stream.fromResource[R, E, A]`
1444
+
1445
+ Uses a ZIO Blocks `Resource[R]` (more abstract, composable resource type) within a stream:
1446
+
1447
+ ```scala
1448
+ object Stream {
1449
+ def fromResource[R, E, A](resource: Resource[R])(use: R => Stream[E, A]): Stream[E, A]
1450
+ }
1451
+ ```
1452
+
1453
+ Use `fromResource` when you already have a `Resource` value, or when you need resource composition. The resource is acquired at stream start and released when the stream terminates:
1454
+
1455
+ ```scala
1456
+ import zio.blocks.streams.*
1457
+ import zio.blocks.scope.Resource
1458
+
1459
+ val resource = Resource.acquireRelease(acquire = {
1460
+ println("Acquiring resource")
1461
+ 42
1462
+ })(release = { value =>
1463
+ println(s"Releasing resource with value: $value")
1464
+ })
1465
+
1466
+ val stream = Stream.fromResource(resource) { value =>
1467
+ Stream(value, value * 2, value * 3)
1468
+ }
1469
+
1470
+ val result = stream.runCollect
1471
+ ```
1472
+
1473
+ ### `Stream#ensuring`
1474
+
1475
+ Adds a **cleanup action to any stream**, regardless of how it is created. The finalizer runs in a `finally` block:
1476
+
1477
+ ```scala
1478
+ trait Stream[+E, +A] {
1479
+ def ensuring(finalizer: => Unit): Stream[E, A]
1480
+ }
1481
+ ```
1482
+
1483
+ Use `ensuring` for simple cleanup tasks that don't fit the acquire-release pattern:
1484
+
1485
+ ```scala
1486
+ import zio.blocks.streams.*
1487
+
1488
+ val stream = Stream(1, 2, 3)
1489
+ .ensuring {
1490
+ println("Stream finished (success or error)")
1491
+ }
1492
+
1493
+ val result = stream.runCollect
1494
+ ```
1495
+
1496
+ The finalizer always runs, in a `finally` block:
1497
+
1498
+ ```scala
1499
+ import zio.blocks.streams.*
1500
+
1501
+ val managed = Stream(1, 2, 3)
1502
+ .ensuring { println("Cleaned up") }
1503
+
1504
+ val result = managed.runCollect
1505
+ ```
1506
+
1507
+ ## Running Streams
1508
+
1509
+ All terminal operations are synchronous and return `Either[E, Z]`. The error type is the union of the stream's error type and any sink-specific error type.
1510
+
1511
+ ### Collecting Results
1512
+
1513
+ These operations accumulate or examine stream results, running the entire stream to completion:
1514
+
1515
+ #### `Stream#runCollect`
1516
+
1517
+ Collects all elements into a `Chunk[A]`:
1518
+
1519
+ ```scala
1520
+ trait Stream[+E, +A] {
1521
+ def runCollect: Either[E, Chunk[A]]
1522
+ }
1523
+ ```
1524
+
1525
+ This is the most common terminal operation for extracting results:
1526
+
1527
+ ```scala
1528
+ import zio.blocks.streams.*
1529
+
1530
+ val nums = Stream(1, 2, 3, 4, 5)
1531
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5)
1532
+ val result = nums.runCollect
1533
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3, 4, 5))
1534
+ // result is Right(Chunk(1, 2, 3, 4, 5))
1535
+ ```
1536
+
1537
+ #### `Stream#run[E2, Z]`
1538
+
1539
+ Runs the stream with a custom sink, producing result `Z`:
1540
+
1541
+ ```scala
1542
+ trait Stream[+E, +A] {
1543
+ def run[E2 >: E, Z](sink: Sink[E2, A, Z]): Either[E2, Z]
1544
+ }
1545
+ ```
1546
+
1547
+ Use `run` when you need a specialized sink operation:
1548
+
1549
+ ```scala
1550
+ import zio.blocks.streams.*
1551
+
1552
+ val nums = Stream(1, 2, 3, 4, 5)
1553
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5)
1554
+ val sum = nums.run(Sink.foldLeft(0)((acc, x) => acc + x))
1555
+ // sum: Either[Nothing, Int] = Right(15)
1556
+ // sum is Right(15)
1557
+ ```
1558
+
1559
+ ### Discarding Results
1560
+
1561
+ These operations consume streams without collecting their elements, useful when you only care about side effects:
1562
+
1563
+ #### `Stream#runDrain`
1564
+
1565
+ Consumes all elements and discards them, returning `Unit`:
1566
+
1567
+ ```scala
1568
+ trait Stream[+E, +A] {
1569
+ def runDrain: Either[E, Unit]
1570
+ }
1571
+ ```
1572
+
1573
+ Use `runDrain` when you only care about side effects:
1574
+
1575
+ ```scala
1576
+ import zio.blocks.streams.*
1577
+
1578
+ val nums = Stream(1, 2, 3)
1579
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3)
1580
+ val sideEffect = nums.tapEach(x => println(s"Processing $x"))
1581
+ // sideEffect: Stream[Nothing, Int] = Stream(1, 2, 3).map(...)
1582
+ val result = sideEffect.runDrain
1583
+ // Processing 1
1584
+ // Processing 2
1585
+ // Processing 3
1586
+ // result: Either[Nothing, Unit] = Right(())
1587
+ ```
1588
+
1589
+ #### `Stream#runForeach`
1590
+
1591
+ Applies a function to each element for side effects:
1592
+
1593
+ ```scala
1594
+ trait Stream[+E, +A] {
1595
+ def runForeach(f: A => Unit): Either[E, Unit]
1596
+ }
1597
+ ```
1598
+
1599
+ Alias `foreach` also exists:
1600
+
1601
+ ```scala
1602
+ import zio.blocks.streams.*
1603
+
1604
+ val nums = Stream(1, 2, 3)
1605
+ val result = nums.foreach(x => println(s"Got: $x"))
1606
+ ```
1607
+
1608
+ ### Aggregations
1609
+
1610
+ These operations reduce streams to single values, aggregating elements into results:
1611
+
1612
+ #### `Stream#runFold[Z]`
1613
+
1614
+ Folds all elements using an accumulator, returning the final result:
1615
+
1616
+ ```scala
1617
+ trait Stream[+E, +A] {
1618
+ def runFold[Z](z: Z)(f: (Z, A) => Z): Either[E, Z]
1619
+ }
1620
+ ```
1621
+
1622
+ This is the most general aggregation, equivalent to `reduce` or `fold` on eager sequences:
1623
+
1624
+ ```scala
1625
+ import zio.blocks.streams.*
1626
+
1627
+ val nums = Stream(1, 2, 3, 4)
1628
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4)
1629
+ val sum = nums.runFold(0)(_ + _)
1630
+ // sum: Either[Nothing, Int] = Right(10)
1631
+ ```
1632
+
1633
+ Specialized overloads for primitives avoid boxing:
1634
+
1635
+ ```scala
1636
+ def runFold(z: Int)(f: (Int, A) => Int): Either[E, Int]
1637
+ def runFold(z: Long)(f: (Long, A) => Long): Either[E, Long]
1638
+ def runFold(z: Double)(f: (Double, A) => Double): Either[E, Double]
1639
+ ```
1640
+
1641
+ #### `Stream#count`
1642
+
1643
+ Returns the number of elements:
1644
+
1645
+ ```scala
1646
+ trait Stream[+E, +A] {
1647
+ def count: Either[E, Long]
1648
+ }
1649
+ ```
1650
+
1651
+ Counting elements in a stream:
1652
+
1653
+ ```scala
1654
+ import zio.blocks.streams.*
1655
+
1656
+ val nums = Stream(10, 20, 30, 40, 50)
1657
+ // nums: Stream[Nothing, Int] = Stream(10, 20, 30, 40, 50)
1658
+ val total = nums.count
1659
+ // total: Either[Nothing, Long] = Right(5L)
1660
+ ```
1661
+
1662
+ #### `Stream#head`
1663
+
1664
+ Returns the first element (or `None` if empty):
1665
+
1666
+ ```scala
1667
+ trait Stream[+E, +A] {
1668
+ def head: Either[E, Option[A]]
1669
+ }
1670
+ ```
1671
+
1672
+ Getting the first element without collecting the entire stream:
1673
+
1674
+ ```scala
1675
+ import zio.blocks.streams.*
1676
+
1677
+ val nums = Stream(10, 20, 30, 40, 50)
1678
+ // nums: Stream[Nothing, Int] = Stream(10, 20, 30, 40, 50)
1679
+ val first = nums.head
1680
+ // first: Either[Nothing, Option[Int]] = Right(Some(10))
1681
+ ```
1682
+
1683
+ #### `Stream#last`
1684
+
1685
+ Returns the last element (or `None` if empty):
1686
+
1687
+ ```scala
1688
+ trait Stream[+E, +A] {
1689
+ def last: Either[E, Option[A]]
1690
+ }
1691
+ ```
1692
+
1693
+ Getting the final element of the stream:
1694
+
1695
+ ```scala
1696
+ import zio.blocks.streams.*
1697
+
1698
+ val nums = Stream(10, 20, 30, 40, 50)
1699
+ // nums: Stream[Nothing, Int] = Stream(10, 20, 30, 40, 50)
1700
+ val last = nums.last
1701
+ // last: Either[Nothing, Option[Int]] = Right(Some(50))
1702
+ ```
1703
+
1704
+ #### `Stream#find[A]`
1705
+
1706
+ Returns the first element satisfying a predicate:
1707
+
1708
+ ```scala
1709
+ trait Stream[+E, +A] {
1710
+ def find(pred: A => Boolean): Either[E, Option[A]]
1711
+ }
1712
+ ```
1713
+
1714
+ Finding the first element matching a condition:
1715
+
1716
+ ```scala
1717
+ import zio.blocks.streams.*
1718
+
1719
+ val nums = Stream(10, 20, 30, 40, 50)
1720
+ // nums: Stream[Nothing, Int] = Stream(10, 20, 30, 40, 50)
1721
+ val firstEven = nums.find(_ % 2 == 0)
1722
+ // firstEven: Either[Nothing, Option[Int]] = Right(Some(10))
1723
+ ```
1724
+
1725
+ #### `Stream#exists[A]`
1726
+
1727
+ Returns `true` if any element satisfies a predicate, short-circuiting:
1728
+
1729
+ ```scala
1730
+ trait Stream[+E, +A] {
1731
+ def exists(pred: A => Boolean): Either[E, Boolean]
1732
+ }
1733
+ ```
1734
+
1735
+ Checking if any element is greater than 35:
1736
+
1737
+ ```scala
1738
+ import zio.blocks.streams.*
1739
+
1740
+ val nums = Stream(10, 20, 30, 40, 50)
1741
+ // nums: Stream[Nothing, Int] = Stream(10, 20, 30, 40, 50)
1742
+ val hasLargeValue = nums.exists(_ > 35)
1743
+ // hasLargeValue: Either[Nothing, Boolean] = Right(true)
1744
+ ```
1745
+
1746
+ #### `Stream#forall[A]`
1747
+
1748
+ Returns `true` if all elements satisfy a predicate, short-circuiting:
1749
+
1750
+ ```scala
1751
+ trait Stream[+E, +A] {
1752
+ def forall(pred: A => Boolean): Either[E, Boolean]
1753
+ }
1754
+ ```
1755
+
1756
+ Checking if all elements are positive:
1757
+
1758
+ ```scala
1759
+ import zio.blocks.streams.*
1760
+
1761
+ val nums = Stream(10, 20, 30, 40, 50)
1762
+ // nums: Stream[Nothing, Int] = Stream(10, 20, 30, 40, 50)
1763
+ val allPositive = nums.forall(_ > 0)
1764
+ // allPositive: Either[Nothing, Boolean] = Right(true)
1765
+ ```
1766
+
1767
+ ## Integration with Pipeline and Sink
1768
+
1769
+ Streams compose with pipelines and sinks to form complete data processing flows:
1770
+
1771
+ ### Using Pipelines
1772
+
1773
+ `via[B]` — Applies a `Pipeline[A, B]` transformation to the stream.:
1774
+
1775
+ ```scala
1776
+ trait Stream[+E, +A] {
1777
+ final def via[B](pipe: Pipeline[A, B]): Stream[E, B]
1778
+ }
1779
+ ```
1780
+
1781
+ Pipelines are composable transformations that can be reused across streams and sinks. Common pipelines include `Pipeline.map`, `Pipeline.filter`, `Pipeline.take`, and `Pipeline.drop`:
1782
+
1783
+ ```scala
1784
+ import zio.blocks.streams.*
1785
+
1786
+ val nums = Stream(1, 2, 3, 4, 5)
1787
+ // nums: Stream[Nothing, Int] = Stream(1, 2, 3, 4, 5)
1788
+ val pipe = Pipeline.filter((x: Int) => x > 2).andThen(Pipeline.map(_ * 10))
1789
+ // pipe: Pipeline[Int, Int] = zio.blocks.streams.Pipeline$Composed@60361815
1790
+ val result = nums.via(pipe).runCollect
1791
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(30, 40, 50))
1792
+ ```
1793
+
1794
+ Pipelines are useful when you want to build reusable transformation logic:
1795
+
1796
+ ```scala
1797
+ import zio.blocks.streams.*
1798
+
1799
+ def positiveIntsPipe: Pipeline[Int, Int] =
1800
+ Pipeline.filter((x: Int) => x > 0)
1801
+
1802
+ val mixed = Stream(-2, -1, 0, 1, 2)
1803
+ // mixed: Stream[Nothing, Int] = Stream(-2, -1, 0, 1, 2)
1804
+ val positives = mixed.via(positiveIntsPipe)
1805
+ // positives: Stream[Nothing, Int] = Stream(-2, -1, 0, 1, 2).filter(...)
1806
+ val result = positives.runCollect
1807
+ // result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2))
1808
+ ```
1809
+
1810
+ ### Understanding Sinks
1811
+
1812
+ A `Sink[+E, -A, +Z]` is a consumer of elements of type `A` that produces a result `Z` or fails with `E`. Sinks are contravariant in `A` (they can accept a supertype of what they expect). Common sinks include:
1813
+
1814
+ - `Sink.collectAll: Sink[Nothing, A, Chunk[A]]` — collects all elements
1815
+ - `Sink.drain: Sink[Nothing, A, Unit]` — discards all elements
1816
+ - `Sink.count: Sink[Nothing, Any, Long]` — counts elements
1817
+ - `Sink.foldLeft: Sink[Nothing, A, Z]` — folds elements with an accumulator
1818
+ - `Sink.head: Sink[Nothing, A, Option[A]]` — takes the first element
1819
+ - `Sink.foreach: Sink[Nothing, A, Unit]` — applies a function to each element
1820
+
1821
+ When you call `stream.run(sink)`, the stream is compiled to a `Reader` and the sink drains it, consuming all elements and producing the result.
1822
+
1823
+ ## Low-Level Pull with Reader
1824
+
1825
+ `Reader[+Elem]` is the low-level, pull-based source that backs every stream at execution time. Most users never interact with `Reader` directly — it is the compilation target when a stream runs. However, you can open a stream for manual element-by-element pulling using `start` with a `Scope`.
1826
+
1827
+ ### Manual Pull via `start`
1828
+
1829
+ `start` — Opens a stream for manual pulling within a `Scope`. The reader is closed automatically when the scope exits.:
1830
+
1831
+ ```scala
1832
+ trait Stream[+E, +A] {
1833
+ def start(using scope: Scope): scope.$[Reader[A]]
1834
+ }
1835
+ ```
1836
+
1837
+ Use `start` to manually pull elements within a resource scope:
1838
+
1839
+ ```scala title="streams-examples/src/main/scala/stream/ManualPullUsingStart.scala"
1840
+ /*
1841
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1842
+ *
1843
+ * Licensed under the Apache License, Version 2.0 (the "License");
1844
+ * you may not use this file except in compliance with the License.
1845
+ * You may obtain a copy of the License at
1846
+ *
1847
+ * http://www.apache.org/licenses/LICENSE-2.0
1848
+ *
1849
+ * Unless required by applicable law or agreed to in writing, software
1850
+ * distributed under the License is distributed on an "AS IS" BASIS,
1851
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1852
+ * See the License for the specific language governing permissions and
1853
+ * limitations under the License.
1854
+ */
1855
+
1856
+ package stream
1857
+
1858
+ import zio.blocks.streams.*
1859
+ import zio.blocks.streams.io.Reader
1860
+ import zio.blocks.scope.*
1861
+
1862
+ object ManualPullUsingStart extends App {
1863
+ Scope.global.scoped { scope =>
1864
+ import scope.*
1865
+
1866
+ // Open a stream for manual pulling
1867
+ val reader: $[Reader[Int]] = Stream.range(1, 6).start(using scope)
1868
+
1869
+ $(reader) { r =>
1870
+ // Iterate through reader values using the protocol directly
1871
+ // (cannot use scoped value in nested function, so use it directly)
1872
+ var current = r.read(-1)
1873
+ while (current != -1) {
1874
+ println(current) // prints 1, 2, 3, 4, 5
1875
+ current = r.read(-1)
1876
+ }
1877
+ }
1878
+ // reader is closed automatically when scope exits
1879
+ }
1880
+ }
1881
+ ```
1882
+
1883
+ Use `Stream#start` when you need element-by-element control rather than running through a Sink. The returned Reader is closed automatically when the scope closes.
1884
+
1885
+ ### The Reader Protocol
1886
+
1887
+ The pull protocol uses a **sentinel value** to signal end-of-stream:
1888
+
1889
+ - `read(sentinel)` — returns the next element, or `sentinel` when exhausted
1890
+ - `close()` — signals the consumer is done
1891
+ - `isClosed` — checks whether the reader is closed
1892
+
1893
+ For primitive types, specialized methods avoid boxing:
1894
+
1895
+ - `readInt(sentinel: Long): Long`
1896
+ - `readLong(sentinel: Long): Long`
1897
+ - `readFloat(sentinel: Double): Double`
1898
+ - `readDouble(sentinel: Double): Double`
1899
+
1900
+ :::note
1901
+ Avoid holding references to a `Reader` obtained via `start` outside its `Scope`. The scope guarantees cleanup; escaping the reader defeats that guarantee.
1902
+ :::
1903
+
1904
+ ## Implementation Notes
1905
+
1906
+ ZIO Blocks Streams achieves zero-boxing via compile-time type detection and dual compilation strategies:
1907
+
1908
+ ### JVM Primitive Specialization
1909
+
1910
+ By default, Scala's type system boxes primitive values (Int, Long, Double, etc.) into objects, which wastes memory and is slower. ZIO Blocks' `Stream` uses `JvmType.Infer[A]` (a compile-time implicit) to detect primitive types at compile time and dispatch to unboxed, specialized implementations.
1911
+
1912
+ For example, `Stream#map`, `Stream#filter`, and `Stream#scan` all have specialized branches for `JvmType.Int` that use `readInt(Long.MinValue)` instead of boxing:
1913
+
1914
+ ```scala
1915
+ if (jvmType eq JvmType.Int) {
1916
+ val i = source.readInt(Long.MinValue)(using unsafeEvidence)
1917
+ // ... unboxed, fast path
1918
+ } else {
1919
+ val o = reader.read(EndOfStream) // generic boxed path
1920
+ // ...
1921
+ }
1922
+ ```
1923
+
1924
+ This optimization is transparent: you write normal, high-level code, and the compiler and runtime automatically use the fast path for primitives.
1925
+
1926
+ ### Dual Compilation: Recursive vs Interpreter
1927
+
1928
+ Each stream node compiles in two ways:
1929
+
1930
+ 1. **Recursive (`compile`)**: Builds a tree of `Reader` objects, where each operation wraps the previous one. This is fast for shallow pipelines (< 100 operations).
1931
+
1932
+ 2. **Flat-Array Interpreter (`compileInterpreter`)**: For deep pipelines (> 100 operations), the recursive approach hits Scala's default stack-depth limit (~100) and risks `StackOverflowError`. Instead, the interpreter compiles the entire pipeline into a flat array of operations, executed iteratively.
1933
+
1934
+ The switch happens at `DepthCutoff = 100`. You should never see this in normal use, but it ensures that pipelines of any depth are safe.
1935
+
1936
+ ## Running the Examples
1937
+
1938
+ All code from this guide is available as runnable examples in the `schema-examples` module.
1939
+
1940
+ Clone the repository and navigate to the project:
1941
+
1942
+ ```bash
1943
+ git clone https://github.com/zio/zio-blocks.git
1944
+ cd zio-blocks
1945
+ ```
1946
+
1947
+ **2. Run individual examples with sbt.** Here are the available examples:
1948
+
1949
+ ---
1950
+
1951
+ ### Basic Usage
1952
+
1953
+ This example demonstrates constructing streams from collections, transforming elements with `Stream#map` and `Stream#filter`, and collecting results:
1954
+
1955
+ ```scala title="streams-examples/src/main/scala/stream/StreamBasicUsageExample.scala"
1956
+ /*
1957
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1958
+ *
1959
+ * Licensed under the Apache License, Version 2.0 (the "License");
1960
+ * you may not use this file except in compliance with the License.
1961
+ * You may obtain a copy of the License at
1962
+ *
1963
+ * http://www.apache.org/licenses/LICENSE-2.0
1964
+ *
1965
+ * Unless required by applicable law or agreed to in writing, software
1966
+ * distributed under the License is distributed on an "AS IS" BASIS,
1967
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1968
+ * See the License for the specific language governing permissions and
1969
+ * limitations under the License.
1970
+ */
1971
+
1972
+ package stream
1973
+
1974
+ import zio.blocks.streams.Stream
1975
+ import zio.sbt.ExprEval.show
1976
+
1977
+ object StreamBasicUsageExample extends App {
1978
+ println("=== Stream Basic Usage ===\n")
1979
+
1980
+ // Construction from values
1981
+ println("1. Creating a stream from values:")
1982
+ val nums = Stream(1, 2, 3, 4, 5)
1983
+ show(nums.runCollect)
1984
+
1985
+ // Map transformation
1986
+ println("\n2. Transforming with map:")
1987
+ val doubled = Stream(1, 2, 3).map(_ * 2)
1988
+ show(doubled.runCollect)
1989
+
1990
+ // Filter operation
1991
+ println("\n3. Filtering elements:")
1992
+ val evens = Stream(1, 2, 3, 4, 5, 6).filter(_ % 2 == 0)
1993
+ show(evens.runCollect)
1994
+
1995
+ // Chaining operations
1996
+ println("\n4. Chaining multiple operations:")
1997
+ val result = Stream(1, 2, 3, 4, 5)
1998
+ .map(_ * 2)
1999
+ .filter(_ > 4)
2000
+ .runCollect
2001
+ show(result)
2002
+
2003
+ // Count operation
2004
+ println("\n5. Counting elements:")
2005
+ val count = Stream(1, 2, 3, 4, 5).count
2006
+ show(count)
2007
+
2008
+ // Take operation (short-circuiting)
2009
+ println("\n6. Taking first n elements (short-circuits):")
2010
+ val first3 = Stream.range(0, 1000).take(3).runCollect
2011
+ show(first3)
2012
+
2013
+ // Drop operation
2014
+ println("\n7. Dropping first n elements:")
2015
+ val afterDrop = Stream(1, 2, 3, 4, 5).drop(2).runCollect
2016
+ show(afterDrop)
2017
+
2018
+ // Empty stream
2019
+ println("\n8. Working with empty streams:")
2020
+ val empty = Stream.empty.runCollect
2021
+ show(empty)
2022
+
2023
+ // Concatenation
2024
+ println("\n9. Concatenating streams:")
2025
+ val combined = (Stream(1, 2) ++ Stream(3, 4)).runCollect
2026
+ show(combined)
2027
+ }
2028
+ ```
2029
+
2030
+ To run this example:
2031
+
2032
+ ```bash
2033
+ sbt "streams-examples/runMain stream.StreamBasicUsageExample"
2034
+ ```
2035
+
2036
+ ### Flat-Mapping Nested Streams
2037
+
2038
+ This example shows how `Stream#flatMap` sequences multiple streams and flattens the results:
2039
+
2040
+ ```scala title="streams-examples/src/main/scala/stream/StreamFlatMapExample.scala"
2041
+ /*
2042
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
2043
+ *
2044
+ * Licensed under the Apache License, Version 2.0 (the "License");
2045
+ * you may not use this file except in compliance with the License.
2046
+ * You may obtain a copy of the License at
2047
+ *
2048
+ * http://www.apache.org/licenses/LICENSE-2.0
2049
+ *
2050
+ * Unless required by applicable law or agreed to in writing, software
2051
+ * distributed under the License is distributed on an "AS IS" BASIS,
2052
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2053
+ * See the License for the specific language governing permissions and
2054
+ * limitations under the License.
2055
+ */
2056
+
2057
+ package stream
2058
+
2059
+ import zio.blocks.streams.Stream
2060
+ import zio.sbt.ExprEval.show
2061
+
2062
+ object StreamFlatMapExample extends App {
2063
+ println("=== Stream FlatMap and Nested Streams ===\n")
2064
+
2065
+ // Basic flatMap
2066
+ println("1. Basic flatMap - expand each element into a stream:")
2067
+ val expanded = Stream(1, 2, 3).flatMap(x => Stream(x, x * 10))
2068
+ show(expanded.runCollect)
2069
+
2070
+ // FlatMap with different stream sizes
2071
+ println("\n2. FlatMap with varying sizes:")
2072
+ val varySizes = Stream(1, 2, 3).flatMap(x => Stream.range(0, x))
2073
+ show(varySizes.runCollect)
2074
+
2075
+ // FlatMap with string expansion
2076
+ println("\n3. Expanding into string streams:")
2077
+ val ids = Stream("a", "b")
2078
+ val expanded_ids = ids.flatMap(id => Stream(s"${id}_1", s"${id}_2", s"${id}_3"))
2079
+ show(expanded_ids.runCollect)
2080
+
2081
+ // FlattenAll for deeply nested streams
2082
+ println("\n4. Flattening nested streams with flattenAll:")
2083
+ val nested = Stream(
2084
+ Stream(1, 2),
2085
+ Stream(3, 4),
2086
+ Stream(5, 6)
2087
+ )
2088
+ val flat = Stream.flattenAll(nested)
2089
+ show(flat.runCollect)
2090
+
2091
+ // Sequential processing guarantees
2092
+ println("\n5. Sequential processing (important for side effects and resources):")
2093
+ var order = scala.collection.mutable.Buffer[String]()
2094
+ val tracked = Stream(1, 2, 3).flatMap { x =>
2095
+ order += s"expand($x)"
2096
+ Stream(x, x + 100).tapEach(y => order += s"emit($y)")
2097
+ }
2098
+ val _ = tracked.runCollect
2099
+ show(order.toList)
2100
+
2101
+ // FlatMap with error recovery
2102
+ println("\n6. FlatMap can propagate errors:")
2103
+ sealed trait Error
2104
+ case object InvalidId extends Error
2105
+
2106
+ val mayFail = Stream(1, 2, -1, 3).flatMap { x =>
2107
+ if (x < 0) Stream.fail(InvalidId)
2108
+ else Stream(x, x * 2)
2109
+ }
2110
+ show(mayFail.runCollect)
2111
+ }
2112
+ ```
2113
+
2114
+ Run this example:
2115
+
2116
+ ```bash
2117
+ sbt "streams-examples/runMain stream.StreamFlatMapExample"
2118
+ ```
2119
+
2120
+ ### Error Handling
2121
+
2122
+ This example demonstrates typed error recovery with `fail`, `catchAll`, and `orElse`:
2123
+
2124
+ ```scala title="streams-examples/src/main/scala/stream/StreamErrorHandlingExample.scala"
2125
+ /*
2126
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
2127
+ *
2128
+ * Licensed under the Apache License, Version 2.0 (the "License");
2129
+ * you may not use this file except in compliance with the License.
2130
+ * You may obtain a copy of the License at
2131
+ *
2132
+ * http://www.apache.org/licenses/LICENSE-2.0
2133
+ *
2134
+ * Unless required by applicable law or agreed to in writing, software
2135
+ * distributed under the License is distributed on an "AS IS" BASIS,
2136
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2137
+ * See the License for the specific language governing permissions and
2138
+ * limitations under the License.
2139
+ */
2140
+
2141
+ package stream
2142
+
2143
+ import zio.blocks.streams.Stream
2144
+ import zio.sbt.ExprEval.show
2145
+
2146
+ object StreamErrorHandlingExample extends App {
2147
+ println("=== Stream Error Handling ===\n")
2148
+
2149
+ sealed trait ApiError
2150
+ case object NotFound extends ApiError
2151
+ case class ValidationError(msg: String) extends ApiError
2152
+ case class ServerError(code: Int) extends ApiError
2153
+
2154
+ // Basic fail
2155
+ println("1. Creating a failing stream:")
2156
+ val failed: Stream[ApiError, String] = Stream.fail(NotFound)
2157
+ show(failed.runCollect)
2158
+
2159
+ // catchAll for recovery
2160
+ println("\n2. Recovering from errors with catchAll:")
2161
+ val recovered = Stream.fail(NotFound).catchAll(_ => Stream.succeed("default-value"))
2162
+ show(recovered.runCollect)
2163
+
2164
+ // orElse for recovery
2165
+ println("\n3. Using orElse (lazy fallback evaluation):")
2166
+ val fallback = Stream.fail(NotFound) || Stream(1, 2, 3)
2167
+ show(fallback.runCollect)
2168
+
2169
+ // Error transformation with error-producing flatMap
2170
+ println("\n4. Producing typed errors in flatMap:")
2171
+ val errorExample = Stream(1, 2, 3, 4).flatMap { x =>
2172
+ if (x == 3) Stream.fail[ApiError](ValidationError("cannot process"))
2173
+ else Stream(x)
2174
+ }
2175
+ show(errorExample.runCollect)
2176
+
2177
+ // Handling errors in flatMap chains
2178
+ println("\n5. Error handling in flatMap chains:")
2179
+ val chain = Stream(1, 2, 3, 4).flatMap { x =>
2180
+ if (x == 3) Stream.fail(ValidationError(s"Cannot process $x"))
2181
+ else Stream(x * 10)
2182
+ }
2183
+ show(chain.runCollect)
2184
+
2185
+ // Recovering from errors in flatMap
2186
+ println("\n6. Recovering from errors with catchAll in chains:")
2187
+ val recovered_chain = Stream(1, 2, 3, 4).flatMap { x =>
2188
+ if (x == 3) Stream.fail(ValidationError(s"Cannot process $x"))
2189
+ else Stream(x * 10)
2190
+ }
2191
+ .catchAll(_ => Stream.succeed(-1))
2192
+
2193
+ show(recovered_chain.runCollect)
2194
+
2195
+ // Handling typed errors from attempt
2196
+ println("\n7. Recovering typed errors from Stream.attempt with catchAll:")
2197
+ val risky = Stream.attempt("not-a-number".toInt)
2198
+ val safe = risky.catchAll { case _: NumberFormatException =>
2199
+ Stream.succeed(-1)
2200
+ }
2201
+ show(safe.runCollect)
2202
+
2203
+ // Multiple error branches
2204
+ println("\n8. Distinguishing error types in recovery:")
2205
+ val multi_errors = Stream(1, 2, 3, 4).flatMap { x =>
2206
+ x match {
2207
+ case 2 => Stream.fail(NotFound)
2208
+ case 3 => Stream.fail(ValidationError("Invalid data"))
2209
+ case _ => Stream(x * 10)
2210
+ }
2211
+ }.catchAll {
2212
+ case NotFound => Stream("missing")
2213
+ case ValidationError(msg) => Stream(s"invalid: $msg")
2214
+ case _ => Stream("unknown error")
2215
+ }
2216
+
2217
+ show(multi_errors.runCollect)
2218
+ }
2219
+ ```
2220
+
2221
+ Run this example:
2222
+
2223
+ ```bash
2224
+ sbt "streams-examples/runMain stream.StreamErrorHandlingExample"
2225
+ ```
2226
+
2227
+ ### Resource Management
2228
+
2229
+ This example shows how `fromAcquireRelease` and `ensuring` manage resources safely:
2230
+
2231
+ ```scala title="streams-examples/src/main/scala/stream/StreamResourceExample.scala"
2232
+ /*
2233
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
2234
+ *
2235
+ * Licensed under the Apache License, Version 2.0 (the "License");
2236
+ * you may not use this file except in compliance with the License.
2237
+ * You may obtain a copy of the License at
2238
+ *
2239
+ * http://www.apache.org/licenses/LICENSE-2.0
2240
+ *
2241
+ * Unless required by applicable law or agreed to in writing, software
2242
+ * distributed under the License is distributed on an "AS IS" BASIS,
2243
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2244
+ * See the License for the specific language governing permissions and
2245
+ * limitations under the License.
2246
+ */
2247
+
2248
+ package stream
2249
+
2250
+ import zio.blocks.streams.Stream
2251
+ import zio.sbt.ExprEval.show
2252
+ import scala.collection.mutable.Buffer
2253
+
2254
+ object StreamResourceExample extends App {
2255
+ println("=== Stream Resource Management ===\n")
2256
+
2257
+ // Simulated resource type
2258
+ case class Database(name: String) {
2259
+ private var closed = false
2260
+
2261
+ def query(q: String): List[String] = {
2262
+ if (closed) throw new Exception("Database is closed")
2263
+ q match {
2264
+ case "users" => List("Alice", "Bob", "Charlie")
2265
+ case "ids" => List("1", "2", "3")
2266
+ case _ => List()
2267
+ }
2268
+ }
2269
+
2270
+ def close(): Unit = {
2271
+ println(s" → Closing database: $name")
2272
+ closed = true
2273
+ }
2274
+
2275
+ def isClosed: Boolean = closed
2276
+ }
2277
+
2278
+ // Basic resource management
2279
+ println("1. Basic fromAcquireRelease - automatic cleanup:")
2280
+ val log = Buffer[String]()
2281
+
2282
+ val managed = Stream.fromAcquireRelease(
2283
+ acquire = {
2284
+ log += "opened"
2285
+ Database("main")
2286
+ },
2287
+ release = { db =>
2288
+ db.close()
2289
+ log += "closed"
2290
+ }
2291
+ )(db => Stream.fromIterable(db.query("users")))
2292
+
2293
+ val result1 = managed.runCollect
2294
+ show(result1)
2295
+ show(log.toList)
2296
+
2297
+ // Ensuring cleanup
2298
+ println("\n2. Using ensuring for guaranteed cleanup:")
2299
+ log.clear()
2300
+ var finalizing = false
2301
+
2302
+ val withEnsure = Stream(1, 2, 3)
2303
+ .tapEach(x => log += s"processing $x")
2304
+ .ensuring {
2305
+ finalizing = true
2306
+ log += "finalizing"
2307
+ }
2308
+
2309
+ val result2 = withEnsure.runCollect
2310
+ show(result2)
2311
+ show(log.toList)
2312
+ show(finalizing)
2313
+
2314
+ // Error safety
2315
+ println("\n3. Cleanup happens even on error:")
2316
+ log.clear()
2317
+
2318
+ sealed trait Error
2319
+ case object ProcessingFailed extends Error
2320
+
2321
+ val errorStream = Stream.fromAcquireRelease(
2322
+ acquire = {
2323
+ log += "opened"
2324
+ Database("error-test")
2325
+ },
2326
+ release = { db =>
2327
+ db.close()
2328
+ log += "closed"
2329
+ }
2330
+ )(db =>
2331
+ Stream(1, 2, 3).flatMap { x =>
2332
+ if (x == 2) Stream.fail(ProcessingFailed)
2333
+ else Stream(x)
2334
+ }
2335
+ )
2336
+
2337
+ val result3 = errorStream.runCollect
2338
+ show(result3)
2339
+ show(log.toList)
2340
+
2341
+ // Multiple nested resources
2342
+ println("\n4. Multiple nested resources with proper cleanup order:")
2343
+ log.clear()
2344
+
2345
+ val nested = Stream.fromAcquireRelease(
2346
+ acquire = {
2347
+ log += "open db1"
2348
+ Database("db1")
2349
+ },
2350
+ release = db => {
2351
+ db.close()
2352
+ log += "close db1"
2353
+ }
2354
+ )(db1 =>
2355
+ Stream.fromAcquireRelease(
2356
+ acquire = {
2357
+ log += "open db2"
2358
+ Database("db2")
2359
+ },
2360
+ release = db2 => {
2361
+ db2.close()
2362
+ log += "close db2"
2363
+ }
2364
+ ) { db2 =>
2365
+ val data = db1.query("users") ++ db2.query("ids")
2366
+ Stream.fromIterable(data).tapEach(x => log += s"emit $x")
2367
+ }
2368
+ )
2369
+
2370
+ val result4 = nested.runCollect
2371
+ show(result4)
2372
+ show(log.toList)
2373
+
2374
+ // AutoCloseable integration
2375
+ println("\n5. Using AutoCloseable for simpler cleanup:")
2376
+ log.clear()
2377
+
2378
+ class AutoCloseableDb extends AutoCloseable {
2379
+ def close(): Unit =
2380
+ log += "auto-closed"
2381
+ }
2382
+
2383
+ val autoCloseable = Stream.fromAcquireRelease(
2384
+ acquire = {
2385
+ log += "acquired"
2386
+ new AutoCloseableDb
2387
+ }
2388
+ // release defaults to calling .close() on AutoCloseable
2389
+ )(db => Stream.succeed(42))
2390
+
2391
+ val result5 = autoCloseable.runCollect
2392
+ show(result5)
2393
+ show(log.toList)
2394
+ }
2395
+
2396
+ object X extends App {
2397
+ import zio.blocks.streams.*
2398
+ import java.io.*
2399
+
2400
+ val charCount: Either[IOException, Long] =
2401
+ Stream
2402
+ .fromJavaReader(new StringReader("Hello\nWorld")) // lazily acquires reader
2403
+ .filter(!_.isWhitespace) // process only non-whitespace
2404
+ .count // count all matching characters
2405
+
2406
+ println(charCount) // prints Right(10) — count of non-whitespace characters
2407
+ }
2408
+ ```
2409
+
2410
+ Run this example:
2411
+
2412
+ ```bash
2413
+ sbt "streams-examples/runMain stream.StreamResourceExample"
2414
+ ```
2415
+
2416
+ ### Windowing and Scanning
2417
+
2418
+ This example demonstrates `grouped`, `sliding`, and `scan` for windowing and stateful transformations:
2419
+
2420
+ ```scala title="streams-examples/src/main/scala/stream/StreamWindowingExample.scala"
2421
+ /*
2422
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
2423
+ *
2424
+ * Licensed under the Apache License, Version 2.0 (the "License");
2425
+ * you may not use this file except in compliance with the License.
2426
+ * You may obtain a copy of the License at
2427
+ *
2428
+ * http://www.apache.org/licenses/LICENSE-2.0
2429
+ *
2430
+ * Unless required by applicable law or agreed to in writing, software
2431
+ * distributed under the License is distributed on an "AS IS" BASIS,
2432
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
2433
+ * See the License for the specific language governing permissions and
2434
+ * limitations under the License.
2435
+ */
2436
+
2437
+ package stream
2438
+
2439
+ import zio.blocks.streams.Stream
2440
+ import zio.sbt.ExprEval.show
2441
+
2442
+ object StreamWindowingExample extends App {
2443
+ println("=== Stream Windowing and Stateful Transformations ===\n")
2444
+
2445
+ // Grouped - fixed-size windows
2446
+ println("1. Grouping into fixed-size chunks:")
2447
+ val nums = Stream(1, 2, 3, 4, 5, 6, 7)
2448
+ val grouped = nums.grouped(3)
2449
+ show(grouped.runCollect)
2450
+
2451
+ // Grouped with incomplete last chunk
2452
+ println("\n2. Last chunk may be smaller:")
2453
+ val ungrouped = Stream(1, 2, 3, 4, 5).grouped(2)
2454
+ show(ungrouped.runCollect)
2455
+
2456
+ // Sliding window
2457
+ println("\n3. Sliding window (default step = 1):")
2458
+ val sliding1 = Stream(1, 2, 3, 4, 5).sliding(3)
2459
+ show(sliding1.runCollect)
2460
+
2461
+ // Sliding with custom step
2462
+ println("\n4. Sliding window with step > 1:")
2463
+ val sliding2 = Stream(1, 2, 3, 4, 5, 6, 7, 8).sliding(3, step = 2)
2464
+ show(sliding2.runCollect)
2465
+
2466
+ // Scan - running aggregate
2467
+ println("\n5. Scan for running sum (accumulator pattern):")
2468
+ val cumsum = Stream(1, 2, 3, 4, 5).scan(0)(_ + _)
2469
+ show(cumsum.runCollect)
2470
+
2471
+ // Scan for running product
2472
+ println("\n6. Scan for running product:")
2473
+ val cumprod = Stream(1, 2, 3, 4).scan(1)(_ * _)
2474
+ show(cumprod.runCollect)
2475
+
2476
+ // MapAccum - accumulator + transform
2477
+ println("\n7. MapAccum for indexed transformation:")
2478
+ val indexed = Stream("a", "b", "c").mapAccum(0)((idx, x) => (idx + 1, (idx, x)))
2479
+ show(indexed.runCollect)
2480
+
2481
+ // MapAccum with state structure
2482
+ println("\n8. MapAccum with complex state:")
2483
+ case class Stats(count: Int, sum: Int, max: Int)
2484
+
2485
+ val stats = Stream(5, 3, 8, 2, 9).mapAccum(Stats(0, 0, Int.MinValue)) { case (s, x) =>
2486
+ (
2487
+ Stats(s.count + 1, s.sum + x, math.max(s.max, x)),
2488
+ (x, Stats(s.count + 1, s.sum + x, math.max(s.max, x)))
2489
+ )
2490
+ }
2491
+ show(stats.runCollect)
2492
+
2493
+ // Combining windowing with filtering
2494
+ println("\n9. Windowing + filtering (only windows with sum > 5):")
2495
+ val filtered = Stream(1, 2, 3, 4, 5, 6)
2496
+ .sliding(3, step = 1)
2497
+ .filter(chunk => chunk.foldLeft(0)(_ + _) > 5)
2498
+ show(filtered.runCollect)
2499
+
2500
+ // Chaining scan with other operations
2501
+ println("\n10. Scan + filter for conditional processing:")
2502
+ val conditional = Stream(1, 1, 2, 1, 1, 3)
2503
+ .scan(0)(_ + _)
2504
+ .filter(_ >= 3) // emit when cumsum >= 3
2505
+ show(conditional.runCollect)
2506
+
2507
+ // Intersperse - useful with grouping
2508
+ println("\n11. Intersperse (insert separator between elements):")
2509
+ val separated = Stream(1, 2, 3).intersperse(0)
2510
+ show(separated.runCollect)
2511
+
2512
+ // Grouped + intersperse for row formatting
2513
+ println("\n12. Grouped + intersperse for CSV-like output:")
2514
+ val rows = Stream(1, 2, 3, 4, 5, 6)
2515
+ .grouped(2)
2516
+ .map(chunk => chunk.toList.mkString(","))
2517
+ .intersperse("\n")
2518
+ show(rows.runCollect)
2519
+ }
2520
+ ```
2521
+
2522
+ Run this example:
2523
+
2524
+ ```bash
2525
+ sbt "streams-examples/runMain stream.StreamWindowingExample"
2526
+ ```