@zio.dev/zio-blocks 0.0.32 → 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.
- package/guides/compile-time-resource-safety-with-scope.md +16 -17
- package/guides/getting-started-with-mux.md +1507 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +246 -165
- package/guides/telemetry-guide.md +1069 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +293 -51
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +651 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config.md +158 -0
- package/reference/context.md +4 -4
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +825 -0
- package/reference/endpoint/path-codec.md +237 -0
- package/reference/endpoint/route-pattern.md +196 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +212 -0
- package/reference/html.md +1120 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +855 -0
- package/reference/http-model/index.md +47 -0
- package/reference/http-model/model.md +1481 -0
- package/reference/http-model/schema.md +747 -0
- package/reference/maybe.md +826 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +823 -0
- package/reference/openapi.md +1351 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -2
- package/reference/resource-management/scope.md +28 -12
- package/reference/resource-management/wire.md +3 -7
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +151 -0
- package/reference/ringbuffer/mpsc.mdx +132 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +344 -0
- package/reference/{allows.md → schema/allows.md} +4 -4
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +2 -3
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +480 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +3 -3
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +34 -192
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +10 -10
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +50 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +5 -5
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +3 -3
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/{schema.md → schema/schema.md} +13 -1
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +533 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +146 -0
- package/reference/sql/db-tx.md +82 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +254 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +353 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/concurrent-operators.md +106 -0
- package/reference/streams/index.md +653 -0
- package/reference/streams/pipeline.md +718 -0
- package/reference/streams/reader.md +1284 -0
- package/reference/streams/scala-2-compatibility.md +55 -0
- package/reference/streams/sink.md +1426 -0
- package/reference/streams/stream.md +2526 -0
- package/reference/streams/writer.md +1045 -0
- package/reference/streams/zero-boxing.md +275 -0
- package/reference/telemetry.md +693 -0
- package/reference/typeid.md +2922 -583
- package/sidebars.js +238 -43
- package/superpowers/plans/2026-03-19-docs-critique-subagent.md +407 -0
- package/superpowers/specs/2026-03-19-docs-critique-subagent-design.md +222 -0
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,1426 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sink
|
|
3
|
+
title: "Sink"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`Sink[+E, -A, +Z]` is a **stream consumer** that reads elements of type `A` and produces a result of type `Z`, potentially failing with an error of type `E`. You pass a sink to [Stream.run](./stream.md) to execute the stream synchronously and get `Either[E, Z]`.
|
|
7
|
+
|
|
8
|
+
`Sink`:
|
|
9
|
+
- Is covariant in `E` (error) and `Z` (result) — these are outputs
|
|
10
|
+
- Is contravariant in `A` (input) — a `Sink[_, Any, _]` accepts any element type
|
|
11
|
+
- Participates in JVM primitive specialization for zero-boxing overhead
|
|
12
|
+
- Provides `Sink#contramap`, `Sink#map`, and `Sink#mapError` for composable transformations
|
|
13
|
+
|
|
14
|
+
Here is the structural shape of the `Sink` type:
|
|
15
|
+
|
|
16
|
+
```scala
|
|
17
|
+
abstract class Sink[+E, -A, +Z] {
|
|
18
|
+
def contramap[A2](g: A2 => A): Sink[E, A2, Z]
|
|
19
|
+
def map[Z2](f: Z => Z2): Sink[E, A, Z2]
|
|
20
|
+
def mapError[E2](f: E => E2): Sink[E2, A, Z]
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
Sink is the terminal piece in the streaming architecture. A [Stream](./stream.md) describes *what* to produce, a [Pipeline](./pipeline.md) describes *how* to transform, and a Sink describes *how to consume*:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
|
|
30
|
+
│ Stream[E, A] │ ──→ │ Pipeline[A, B] │ ──→ │ Sink[E, B, Z]│
|
|
31
|
+
└──────────────┘ └──────────────────┘ └──────────────┘
|
|
32
|
+
│
|
|
33
|
+
┌───────▼──────┐
|
|
34
|
+
│ Either[E, Z] │
|
|
35
|
+
└──────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
When you call `stream.run(sink)`:
|
|
39
|
+
1. The stream compiles into a `Reader` (a low-level pull-based source)
|
|
40
|
+
2. The sink's internal `Sink#drain` method pulls elements in a tight loop until end-of-stream
|
|
41
|
+
3. On success, the result wraps in `Right(z)`
|
|
42
|
+
4. Typed errors (`E`) surface as `Left(e)`, while untyped defects propagate as exceptions
|
|
43
|
+
5. The reader's `close()` runs in a `finally` block, ensuring resource safety
|
|
44
|
+
|
|
45
|
+
## Predefined Sinks
|
|
46
|
+
|
|
47
|
+
These are value sinks (no factory arguments). They work on any element type.
|
|
48
|
+
|
|
49
|
+
### `Sink.drain` — Discard All Elements
|
|
50
|
+
|
|
51
|
+
Consumes every element and discards them. Returns `Unit`:
|
|
52
|
+
|
|
53
|
+
```scala
|
|
54
|
+
object Sink {
|
|
55
|
+
val drain: Sink[Nothing, Any, Unit]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use `Sink.drain` when you only care about side effects (e.g., via `Stream#tapEach`) and not the elements themselves:
|
|
60
|
+
|
|
61
|
+
```scala
|
|
62
|
+
import zio.blocks.streams._
|
|
63
|
+
import scala.collection.mutable.Buffer
|
|
64
|
+
|
|
65
|
+
val log = Buffer[String]()
|
|
66
|
+
// log: Buffer[String] = ArrayBuffer(
|
|
67
|
+
// "Processing: 1",
|
|
68
|
+
// "Processing: 2",
|
|
69
|
+
// "Processing: 3"
|
|
70
|
+
// )
|
|
71
|
+
val result = Stream(1, 2, 3)
|
|
72
|
+
.tapEach(x => log += s"Processing: $x")
|
|
73
|
+
.run(Sink.drain)
|
|
74
|
+
// result: Either[Nothing, Unit] = Right(())
|
|
75
|
+
// result is Right(())
|
|
76
|
+
// log contains: ["Processing: 1", "Processing: 2", "Processing: 3"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `Sink.count` — Count Elements
|
|
80
|
+
|
|
81
|
+
Counts the total number of elements consumed. Returns `Long`:
|
|
82
|
+
|
|
83
|
+
```scala
|
|
84
|
+
object Sink {
|
|
85
|
+
val count: Sink[Nothing, Any, Long]
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Count all elements in a stream:
|
|
90
|
+
|
|
91
|
+
```scala
|
|
92
|
+
import zio.blocks.streams._
|
|
93
|
+
|
|
94
|
+
val result = Stream(1, 2, 3, 4, 5).run(Sink.count)
|
|
95
|
+
// result: Either[Nothing, Long] = Right(5L)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `Sink.sumInt` / `Sink.sumLong` / `Sink.sumFloat` / `Sink.sumDouble` — Typed Numeric Sums
|
|
99
|
+
|
|
100
|
+
Returns the sum of all elements as a numeric type. Each sink accepts the corresponding primitive type:
|
|
101
|
+
|
|
102
|
+
```scala
|
|
103
|
+
object Sink {
|
|
104
|
+
val sumInt: Sink[Nothing, Int, Long]
|
|
105
|
+
val sumLong: Sink[Nothing, Long, Long]
|
|
106
|
+
val sumFloat: Sink[Nothing, Float, Double]
|
|
107
|
+
val sumDouble: Sink[Nothing, Double, Double]
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Note that `Sink.sumInt` returns `Long` (to avoid overflow) and `Sink.sumFloat` returns `Double` (to reduce rounding loss):
|
|
112
|
+
|
|
113
|
+
```scala
|
|
114
|
+
import zio.blocks.streams._
|
|
115
|
+
|
|
116
|
+
val intSum = Stream(1, 2, 3, 4, 5).run(Sink.sumInt)
|
|
117
|
+
// intSum: Either[Nothing, Long] = Right(15L)
|
|
118
|
+
|
|
119
|
+
val doubleSum = Stream(1.5, 2.5, 3.0).run(Sink.sumDouble)
|
|
120
|
+
// doubleSum: Either[Nothing, Double] = Right(7.0)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Construction
|
|
124
|
+
|
|
125
|
+
Sinks are created using factory methods on the companion object. These methods fall into several categories based on what they do:
|
|
126
|
+
|
|
127
|
+
### Collecting
|
|
128
|
+
|
|
129
|
+
Gather elements into collections:
|
|
130
|
+
|
|
131
|
+
#### `Sink.collectAll[A]` — Collect into a Chunk
|
|
132
|
+
|
|
133
|
+
Collects all elements into a `Chunk[A]`:
|
|
134
|
+
|
|
135
|
+
```scala
|
|
136
|
+
object Sink {
|
|
137
|
+
def collectAll[A]: Sink[Nothing, A, Chunk[A]]
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
This is the sink behind `Stream.runCollect`:
|
|
142
|
+
|
|
143
|
+
```scala
|
|
144
|
+
import zio.blocks.streams._
|
|
145
|
+
|
|
146
|
+
val result = Stream(1, 2, 3).run(Sink.collectAll[Int])
|
|
147
|
+
// result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `Sink.take[A]` — Collect First N Elements
|
|
151
|
+
|
|
152
|
+
Collects at most `n` elements into a `Chunk[A]`, then stops (short-circuiting the upstream):
|
|
153
|
+
|
|
154
|
+
```scala
|
|
155
|
+
object Sink {
|
|
156
|
+
def take[A](n: Int): Sink[Nothing, A, Chunk[A]]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Collect only the first three elements from a large stream:
|
|
161
|
+
|
|
162
|
+
```scala
|
|
163
|
+
import zio.blocks.streams._
|
|
164
|
+
|
|
165
|
+
val result = Stream.range(0, 1000).run(Sink.take(3))
|
|
166
|
+
// result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(0, 1, 2))
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Aggregation and Search
|
|
170
|
+
|
|
171
|
+
These sinks combine elements into a single result or search for specific elements within a stream:
|
|
172
|
+
|
|
173
|
+
#### `Sink.foldLeft[A, Z]` — General Left Fold
|
|
174
|
+
|
|
175
|
+
Folds all elements using an accumulator function, starting from initial value `z`:
|
|
176
|
+
|
|
177
|
+
```scala
|
|
178
|
+
object Sink {
|
|
179
|
+
def foldLeft[A, Z](z: Z)(f: (Z, A) => Z): Sink[Nothing, A, Z]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
This is the most general aggregation sink:
|
|
184
|
+
|
|
185
|
+
```scala
|
|
186
|
+
import zio.blocks.streams._
|
|
187
|
+
|
|
188
|
+
val sum = Stream(1, 2, 3, 4).run(Sink.foldLeft(0)(_ + _))
|
|
189
|
+
// sum: Either[Nothing, Int] = Right(10)
|
|
190
|
+
|
|
191
|
+
val concat = Stream("a", "b", "c").run(Sink.foldLeft("")(_ + _))
|
|
192
|
+
// concat: Either[Nothing, String] = Right("abc")
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### `Sink.head[A]` — First Element
|
|
196
|
+
|
|
197
|
+
Returns the first element wrapped in `Some`, or `None` for an empty stream:
|
|
198
|
+
|
|
199
|
+
```scala
|
|
200
|
+
object Sink {
|
|
201
|
+
def head[A]: Sink[Nothing, A, Option[A]]
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Get the first element from a stream, or None if empty:
|
|
206
|
+
|
|
207
|
+
```scala
|
|
208
|
+
import zio.blocks.streams._
|
|
209
|
+
|
|
210
|
+
val first = Stream(10, 20, 30).run(Sink.head[Int])
|
|
211
|
+
// first: Either[Nothing, Option[Int]] = Right(Some(10))
|
|
212
|
+
|
|
213
|
+
val empty = Stream.empty.run(Sink.head[Int])
|
|
214
|
+
// empty: Either[Nothing, Option[Int]] = Right(None)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
#### `Sink.last[A]` — Last Element
|
|
218
|
+
|
|
219
|
+
Returns the last element wrapped in `Some`, or `None` for an empty stream. Must consume all elements:
|
|
220
|
+
|
|
221
|
+
```scala
|
|
222
|
+
object Sink {
|
|
223
|
+
def last[A]: Sink[Nothing, A, Option[A]]
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Get the last element from a stream:
|
|
228
|
+
|
|
229
|
+
```scala
|
|
230
|
+
import zio.blocks.streams._
|
|
231
|
+
|
|
232
|
+
val result = Stream(10, 20, 30).run(Sink.last[Int])
|
|
233
|
+
// result: Either[Nothing, Option[Int]] = Right(Some(30))
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `Sink.find[A]` — First Matching Element
|
|
237
|
+
|
|
238
|
+
Returns the first element satisfying `pred`, or `None`. Short-circuits on first match:
|
|
239
|
+
|
|
240
|
+
```scala
|
|
241
|
+
object Sink {
|
|
242
|
+
def find[A](pred: A => Boolean): Sink[Nothing, A, Option[A]]
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Find the first even number in the stream:
|
|
247
|
+
|
|
248
|
+
```scala
|
|
249
|
+
import zio.blocks.streams._
|
|
250
|
+
|
|
251
|
+
val found = Stream(1, 3, 4, 6).run(Sink.find[Int](_ % 2 == 0))
|
|
252
|
+
// found: Either[Nothing, Option[Int]] = Right(Some(4))
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
#### `Sink.exists[A]` — Any Element Matches
|
|
256
|
+
|
|
257
|
+
Returns `true` if any element satisfies `pred`. Short-circuits on first match:
|
|
258
|
+
|
|
259
|
+
```scala
|
|
260
|
+
object Sink {
|
|
261
|
+
def exists[A](pred: A => Boolean): Sink[Nothing, A, Boolean]
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Check if any element matches a condition:
|
|
266
|
+
|
|
267
|
+
```scala
|
|
268
|
+
import zio.blocks.streams._
|
|
269
|
+
|
|
270
|
+
val hasNegative = Stream(1, -2, 3).run(Sink.exists[Int](_ < 0))
|
|
271
|
+
// hasNegative: Either[Nothing, Boolean] = Right(true)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### `Sink.forall[A]` — All Elements Match
|
|
275
|
+
|
|
276
|
+
Returns `true` if all elements satisfy `pred`. Short-circuits to `false` on first failure:
|
|
277
|
+
|
|
278
|
+
```scala
|
|
279
|
+
object Sink {
|
|
280
|
+
def forall[A](pred: A => Boolean): Sink[Nothing, A, Boolean]
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Test whether all elements satisfy a condition:
|
|
285
|
+
|
|
286
|
+
```scala
|
|
287
|
+
import zio.blocks.streams._
|
|
288
|
+
|
|
289
|
+
val allPositive = Stream(1, 2, 3).run(Sink.forall[Int](_ > 0))
|
|
290
|
+
// allPositive: Either[Nothing, Boolean] = Right(true)
|
|
291
|
+
|
|
292
|
+
val notAll = Stream(1, -2, 3).run(Sink.forall[Int](_ > 0))
|
|
293
|
+
// notAll: Either[Nothing, Boolean] = Right(false)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Effectful
|
|
297
|
+
|
|
298
|
+
These sinks perform side effects during stream consumption:
|
|
299
|
+
|
|
300
|
+
#### `Sink.foreach[A]` — Apply Side Effect to Each Element
|
|
301
|
+
|
|
302
|
+
Applies `f` to every element for side effects. Returns `Unit`:
|
|
303
|
+
|
|
304
|
+
```scala
|
|
305
|
+
object Sink {
|
|
306
|
+
def foreach[A](f: A => Unit): Sink[Nothing, A, Unit]
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Print each element as it is processed:
|
|
311
|
+
|
|
312
|
+
```scala
|
|
313
|
+
import zio.blocks.streams._
|
|
314
|
+
|
|
315
|
+
val result = Stream(1, 2, 3).run(Sink.foreach[Int](x => println(s"Got: $x")))
|
|
316
|
+
// Got: 1
|
|
317
|
+
// Got: 2
|
|
318
|
+
// Got: 3
|
|
319
|
+
// result: Either[Nothing, Unit] = Right(())
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Failing
|
|
323
|
+
|
|
324
|
+
These sinks can be used to produce typed errors or fail under specific conditions:
|
|
325
|
+
|
|
326
|
+
#### `Sink.fail[E]` — Immediately Fail
|
|
327
|
+
|
|
328
|
+
Creates a sink that fails immediately with a typed error, without consuming any elements:
|
|
329
|
+
|
|
330
|
+
```scala
|
|
331
|
+
object Sink {
|
|
332
|
+
def fail[E](e: E): Sink[E, Any, Nothing]
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Use this in conditional sink construction:
|
|
337
|
+
|
|
338
|
+
```scala
|
|
339
|
+
import zio.blocks.streams._
|
|
340
|
+
|
|
341
|
+
val sink: Sink[String, Int, Long] =
|
|
342
|
+
if (false) Sink.count
|
|
343
|
+
else Sink.fail("not ready")
|
|
344
|
+
// sink: Sink[String, Int, Long] = zio.blocks.streams.Sink$$anon$8@a09b6b
|
|
345
|
+
|
|
346
|
+
val result = Stream(1, 2, 3).run(sink)
|
|
347
|
+
// result: Either[String, Long] = Left("not ready")
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### I/O
|
|
351
|
+
|
|
352
|
+
Write elements to Java I/O destinations:
|
|
353
|
+
|
|
354
|
+
#### `Sink.fromOutputStream` — Write Bytes
|
|
355
|
+
|
|
356
|
+
Writes every `Byte` element to a `java.io.OutputStream`:
|
|
357
|
+
|
|
358
|
+
```scala
|
|
359
|
+
object Sink {
|
|
360
|
+
def fromOutputStream(os: java.io.OutputStream): Sink[Nothing, Byte, Unit]
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
The sink does **not** close the stream when done. This is intentional: you own the stream's lifecycle, not the sink. You're responsible for closing it yourself (typically via try-with-resources or explicit `close()` calls) to flush buffers and release system resources. This design gives you flexibility to reuse the stream after the sink finishes, or to coordinate closing with other stream operations:
|
|
364
|
+
|
|
365
|
+
```scala
|
|
366
|
+
import zio.blocks.streams._
|
|
367
|
+
import java.io.ByteArrayOutputStream
|
|
368
|
+
|
|
369
|
+
val bos = new ByteArrayOutputStream()
|
|
370
|
+
// bos: ByteArrayOutputStream = Hi!
|
|
371
|
+
|
|
372
|
+
// Write first batch of bytes
|
|
373
|
+
Stream.fromChunk(zio.blocks.chunk.Chunk[Byte](72, 105)).run(Sink.fromOutputStream(bos))
|
|
374
|
+
// res14: Either[Nothing, Unit] = Right(())
|
|
375
|
+
|
|
376
|
+
// Write second batch to the same stream (reuse it)
|
|
377
|
+
Stream.fromChunk(zio.blocks.chunk.Chunk[Byte](33)).run(Sink.fromOutputStream(bos))
|
|
378
|
+
// res15: Either[Nothing, Unit] = Right(())
|
|
379
|
+
|
|
380
|
+
// When done writing all batches, YOU close the stream
|
|
381
|
+
bos.close()
|
|
382
|
+
|
|
383
|
+
// ByteArrayOutputStream ignores close(), so you can still call toByteArray()
|
|
384
|
+
val allBytes = bos.toByteArray()
|
|
385
|
+
// allBytes: Array[Byte] = Array(72, 105, 33)
|
|
386
|
+
// This works because ByteArrayOutputStream doesn't maintain any closeable resources
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
#### `Sink.fromJavaWriter` — Write Characters
|
|
390
|
+
|
|
391
|
+
Writes every `Char` element to a `java.io.Writer`. Does not close the writer when done — you own its lifecycle:
|
|
392
|
+
|
|
393
|
+
```scala
|
|
394
|
+
object Sink {
|
|
395
|
+
def fromJavaWriter(w: java.io.Writer): Sink[Nothing, Char, Unit]
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Write a stream of characters to a StringWriter and access the accumulated text:
|
|
400
|
+
|
|
401
|
+
```scala
|
|
402
|
+
import zio.blocks.streams._
|
|
403
|
+
import java.io.StringWriter
|
|
404
|
+
|
|
405
|
+
val writer = new StringWriter()
|
|
406
|
+
// writer: StringWriter = Hello World
|
|
407
|
+
|
|
408
|
+
// Write a stream of individual characters
|
|
409
|
+
Stream('H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd')
|
|
410
|
+
.run(Sink.fromJavaWriter(writer))
|
|
411
|
+
// res18: Either[Nothing, Unit] = Right(())
|
|
412
|
+
|
|
413
|
+
// Get the final string
|
|
414
|
+
val result = writer.toString()
|
|
415
|
+
// result: String = "Hello World"
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Like `Sink.fromOutputStream`, this sink intentionally does not close the writer. This gives you control over when to flush or close, allowing you to write multiple streams to the same writer or coordinate lifecycle with other operations.
|
|
419
|
+
|
|
420
|
+
### Custom
|
|
421
|
+
|
|
422
|
+
Advanced low-level use cases with direct reader protocol access:
|
|
423
|
+
|
|
424
|
+
#### `Sink.create[E, A, Z]` — Escape Hatch
|
|
425
|
+
|
|
426
|
+
Creates a sink from a raw function that takes a `Reader[A]` and returns `Z`. This is the low-level escape hatch for writing sinks that cannot be expressed using the built-in factories:
|
|
427
|
+
|
|
428
|
+
```scala
|
|
429
|
+
object Sink {
|
|
430
|
+
def create[E, A, Z](f: Reader[A] => Z): Sink[E, A, Z]
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
:::note
|
|
435
|
+
`Sink.create` gives you direct access to the `Reader`, so you are responsible for using the correct read protocol (`Reader#read(sentinel)` for AnyRef, `Reader#readInt(sentinel)` for Int, etc.). Prefer the built-in sinks when possible.
|
|
436
|
+
:::
|
|
437
|
+
|
|
438
|
+
Here's a custom sink that computes the average of all integers in a stream:
|
|
439
|
+
|
|
440
|
+
```scala
|
|
441
|
+
import zio.blocks.streams._
|
|
442
|
+
import zio.blocks.streams.io.Reader
|
|
443
|
+
|
|
444
|
+
// A custom sink that computes the average of Ints
|
|
445
|
+
val average = Sink.create[Nothing, Int, Double] { reader =>
|
|
446
|
+
def loop(sum: Long, count: Long): (Long, Long) = {
|
|
447
|
+
val v = reader.readInt(Long.MinValue)
|
|
448
|
+
if (v.asInstanceOf[Long] == Long.MinValue) (sum, count)
|
|
449
|
+
else {
|
|
450
|
+
val newSum = sum + v
|
|
451
|
+
loop(newSum, count + 1)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
val (sum, count) = loop(0L, 0L)
|
|
455
|
+
if (count == 0) 0.0 else sum.toDouble / count
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
This example shows how `Sink.create` works. The reader reads elements using `Reader#read[Any](null)` — the sentinel protocol — where `null` signals "read the next element" and the function returns `null` when the stream ends. We accumulate the sum and count via recursion, then return the average. You'd use `Sink.create` when no built-in sink provides the exact aggregation or transformation logic you need — it's powerful but requires understanding the low-level [Reader protocol](./reader.md).
|
|
460
|
+
|
|
461
|
+
## Transforming Sinks
|
|
462
|
+
|
|
463
|
+
Every sink can be transformed using these instance methods:
|
|
464
|
+
|
|
465
|
+
### `Sink#contramap[A2]` — Pre-Process Input
|
|
466
|
+
|
|
467
|
+
Transforms the input elements before they reach the sink. The sink's result and error types are unchanged:
|
|
468
|
+
|
|
469
|
+
```scala
|
|
470
|
+
trait Sink[+E, -A, +Z] {
|
|
471
|
+
def contramap[A2](g: A2 => A): Sink[E, A2, Z]
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
`Sink#contramap` is the dual of `Sink#map`: it transforms what goes *in*, not what comes *out*:
|
|
476
|
+
|
|
477
|
+
```scala
|
|
478
|
+
import zio.blocks.streams._
|
|
479
|
+
|
|
480
|
+
// A sink that counts the length of strings
|
|
481
|
+
val totalLength: Sink[Nothing, String, Long] =
|
|
482
|
+
Sink.sumInt.contramap[String](_.length)
|
|
483
|
+
// totalLength: Sink[Nothing, String, Long] = zio.blocks.streams.Sink$Contramapped@5360fe09
|
|
484
|
+
|
|
485
|
+
val result = Stream("hello", "world").run(totalLength)
|
|
486
|
+
// result: Either[Nothing, Long] = Right(10L)
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### `Sink#map[Z2]` — Transform Result
|
|
490
|
+
|
|
491
|
+
Transforms the result after the sink finishes draining:
|
|
492
|
+
|
|
493
|
+
```scala
|
|
494
|
+
trait Sink[+E, -A, +Z] {
|
|
495
|
+
def map[Z2](f: Z => Z2): Sink[E, A, Z2]
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Transform the result after draining:
|
|
500
|
+
|
|
501
|
+
```scala
|
|
502
|
+
import zio.blocks.streams._
|
|
503
|
+
|
|
504
|
+
val countAsString: Sink[Nothing, Any, String] =
|
|
505
|
+
Sink.count.map(n => s"Total: $n elements")
|
|
506
|
+
// countAsString: Sink[Nothing, Any, String] = zio.blocks.streams.Sink$Mapped@96f085b
|
|
507
|
+
|
|
508
|
+
val result = Stream(1, 2, 3).run(countAsString)
|
|
509
|
+
// result: Either[Nothing, String] = Right("Total: 3 elements")
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
### `Sink#mapError[E2]` — Transform Error
|
|
513
|
+
|
|
514
|
+
Transforms the error channel of a sink:
|
|
515
|
+
|
|
516
|
+
```scala
|
|
517
|
+
trait Sink[+E, -A, +Z] {
|
|
518
|
+
inline def mapError[E2](f: E => E2): Sink[E2, A, Z]
|
|
519
|
+
}
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
This method uses Scala 3's `inline` + `summonFrom` to perform a compile-time check: if `E` is `Nothing` (the sink never fails), the compiler elides the wrapper entirely and returns `this` cast to the new type with zero allocation:
|
|
523
|
+
|
|
524
|
+
```scala
|
|
525
|
+
import zio.blocks.streams._
|
|
526
|
+
|
|
527
|
+
// No-op: drain never fails, so mapError is free
|
|
528
|
+
val mapped = Sink.drain.mapError[String](_.toString)
|
|
529
|
+
// At compile time: this is just a cast, no wrapper allocated
|
|
530
|
+
|
|
531
|
+
// Real mapping: fail can produce errors
|
|
532
|
+
val failing = Sink.fail("oops").mapError[RuntimeException](new RuntimeException(_))
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## Integration with Stream
|
|
536
|
+
|
|
537
|
+
`Stream.run(sink)` is the primary entry point. ZIO Blocks also provides convenience methods on `Stream` that delegate to built-in sinks:
|
|
538
|
+
|
|
539
|
+
| Stream method | Equivalent Sink |
|
|
540
|
+
|------------------------|-----------------------------------|
|
|
541
|
+
| `stream.runCollect` | `stream.run(Sink.collectAll)` |
|
|
542
|
+
| `stream.runDrain` | `stream.run(Sink.drain)` |
|
|
543
|
+
| `stream.runForeach(f)` | `stream.run(Sink.foreach(f))` |
|
|
544
|
+
| `stream.runFold(z)(f)` | `stream.run(Sink.foldLeft(z)(f))` |
|
|
545
|
+
| `stream.count` | `stream.run(Sink.count)` |
|
|
546
|
+
| `stream.head` | `stream.run(Sink.head)` |
|
|
547
|
+
| `stream.last` | `stream.run(Sink.last)` |
|
|
548
|
+
| `stream.find(pred)` | `stream.run(Sink.find(pred))` |
|
|
549
|
+
| `stream.exists(pred)` | `stream.run(Sink.exists(pred))` |
|
|
550
|
+
| `stream.forall(pred)` | `stream.run(Sink.forall(pred))` |
|
|
551
|
+
|
|
552
|
+
The `runFold` method with primitive accumulator types (`Int`, `Long`, `Double`) uses specialized internal sink classes that keep the accumulator unboxed.
|
|
553
|
+
|
|
554
|
+
See [Stream — Running Streams](./stream.md#running-streams) for more details on terminal operations.
|
|
555
|
+
|
|
556
|
+
## Integration with Pipeline
|
|
557
|
+
|
|
558
|
+
A [Pipeline](./pipeline.md) can be applied to a Sink using `Pipeline#andThenSink`, producing a new Sink that pre-processes input elements through the pipeline:
|
|
559
|
+
|
|
560
|
+
```scala
|
|
561
|
+
import zio.blocks.streams._
|
|
562
|
+
import zio.blocks.chunk.Chunk
|
|
563
|
+
|
|
564
|
+
val cleanAndCollect: Sink[Nothing, String, Chunk[String]] =
|
|
565
|
+
Pipeline.map[String, String](_.trim.toLowerCase)
|
|
566
|
+
.andThenSink(Sink.collectAll[String])
|
|
567
|
+
// cleanAndCollect: Sink[Nothing, String, Chunk[String]] = zio.blocks.streams.Sink$Contramapped@2d273af9
|
|
568
|
+
|
|
569
|
+
val result = Stream(" Hello ", " WORLD ").run(cleanAndCollect)
|
|
570
|
+
// result: Either[Nothing, Chunk[String]] = Right(IndexedSeq("hello", "world"))
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
The equivalence law holds: `stream.via(pipe).run(sink) == stream.run(pipe.andThenSink(sink))`.
|
|
574
|
+
|
|
575
|
+
See [Pipeline — Applying to a Sink](./pipeline.md#applying-to-a-sink) for more details.
|
|
576
|
+
|
|
577
|
+
## JVM NIO Sinks
|
|
578
|
+
|
|
579
|
+
The `NioSinks` object (JVM-only) provides sinks for Java NIO (`java.nio`) buffers and channels. These exist because NIO is the standard high-performance I/O mechanism on the JVM: non-blocking, memory-efficient, and capable of handling thousands of concurrent connections. When you're writing to network sockets, memory-mapped files, or other NIO-based resources, these sinks give you a convenient way to drain streams directly into NIO data structures without intermediate allocation or copying.
|
|
580
|
+
|
|
581
|
+
Traditional Java I/O (`OutputStream`, `Writer`) blocks threads and requires manual buffering for efficiency. NIO provides non-blocking channels, but using them directly requires buffer allocation, position management, and explicit flushing. `NioSinks` bridges this gap: `NioSinks.fromChannel` handles buffering automatically (default 8KB), while typed variants like `NioSinks.fromByteBufferInt` and `NioSinks.fromByteBufferLong` eliminate boxing overhead by writing primitives directly to buffers you provide.
|
|
582
|
+
|
|
583
|
+
Choose `NioSinks.fromChannel` when you need to write to network sockets or files and cannot afford to block threads. Choose typed variants when you control buffer allocation and are streaming millions of primitives where boxing would degrade performance. **Important:** Read the Sentinel Value Limitation section below—it describes a hard constraint that affects your choice depending on whether your data can contain specific values.
|
|
584
|
+
|
|
585
|
+
Here are the available NIO sinks:
|
|
586
|
+
|
|
587
|
+
```scala
|
|
588
|
+
object NioSinks {
|
|
589
|
+
def fromByteBuffer (buf: ByteBuffer): Sink[Nothing, Byte, Unit]
|
|
590
|
+
def fromByteBufferInt (buf: ByteBuffer): Sink[Nothing, Int, Unit]
|
|
591
|
+
def fromByteBufferLong (buf: ByteBuffer): Sink[Nothing, Long, Unit]
|
|
592
|
+
def fromByteBufferFloat (buf: ByteBuffer): Sink[Nothing, Float, Unit]
|
|
593
|
+
def fromByteBufferDouble(buf: ByteBuffer): Sink[Nothing, Double, Unit]
|
|
594
|
+
def fromChannel(ch: WritableByteChannel, bufSize: Int = 8192): Sink[IOException, Byte, Unit]
|
|
595
|
+
}
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### From ByteBuffer Sinks
|
|
599
|
+
|
|
600
|
+
**`NioSinks.fromByteBuffer` and typed variants** — Write primitive streams directly into a pre-allocated NIO ByteBuffer:
|
|
601
|
+
- `NioSinks.fromByteBuffer` — writes individual `Byte` elements using a read sentinel of `-1`. Use only for unstructured byte data.
|
|
602
|
+
- `NioSinks.fromByteBufferInt`, `NioSinks.fromByteBufferLong`, `NioSinks.fromByteBufferFloat`, `NioSinks.fromByteBufferDouble` — write primitives directly using the buffer's native methods (`putInt`, `putLong`, etc.). These avoid boxing and are faster than the byte variant.
|
|
603
|
+
|
|
604
|
+
Here's an example using ByteBuffer with typed primitive writes:
|
|
605
|
+
|
|
606
|
+
```scala
|
|
607
|
+
import zio.blocks.streams._
|
|
608
|
+
import zio.blocks.streams.NioSinks
|
|
609
|
+
import java.nio.ByteBuffer
|
|
610
|
+
import java.nio.ByteOrder
|
|
611
|
+
|
|
612
|
+
val buffer = ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN)
|
|
613
|
+
// buffer: ByteBuffer = java.nio.HeapByteBuffer[pos=32 lim=32 cap=32]
|
|
614
|
+
|
|
615
|
+
// Write a stream of Longs to the buffer
|
|
616
|
+
Stream(1L, 2L, 3L, 4L).run(NioSinks.fromByteBufferLong(buffer))
|
|
617
|
+
// res25: Either[Nothing, Unit] = Right(())
|
|
618
|
+
|
|
619
|
+
// After writing, rewind to read
|
|
620
|
+
buffer.rewind()
|
|
621
|
+
// res26: ByteBuffer = java.nio.HeapByteBuffer[pos=32 lim=32 cap=32]
|
|
622
|
+
|
|
623
|
+
val readBack = List(
|
|
624
|
+
buffer.getLong(),
|
|
625
|
+
buffer.getLong(),
|
|
626
|
+
buffer.getLong(),
|
|
627
|
+
buffer.getLong()
|
|
628
|
+
)
|
|
629
|
+
// readBack: List[Long] = List(1L, 2L, 3L, 4L)
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
This example allocates a 32-byte buffer (4 Longs × 8 bytes each), writes four `Long` values using `NioSinks.fromByteBufferLong` (which efficiently calls `putLong` on each element), then rewinds and reads them back to verify. The typed variant is significantly faster than `NioSinks.fromByteBuffer` because it operates at the primitive level — no boxing, no element-by-element byte writing.
|
|
633
|
+
|
|
634
|
+
The following example shows streaming voltage sensor readings through a calibration curve and buffering them for downstream computation. When processing sensor arrays or scientific measurements, pre-allocated buffers with typed sinks enable zero-copy batch processing.
|
|
635
|
+
|
|
636
|
+
Here is the complete example:
|
|
637
|
+
|
|
638
|
+
```scala title="streams-examples/src/main/scala/sink/SinkScientificComputingExample.scala"
|
|
639
|
+
/*
|
|
640
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
641
|
+
*
|
|
642
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
643
|
+
* you may not use this file except in compliance with the License.
|
|
644
|
+
* You may obtain a copy of the License at
|
|
645
|
+
*
|
|
646
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
647
|
+
*
|
|
648
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
649
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
650
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
651
|
+
* See the License for the specific language governing permissions and
|
|
652
|
+
* limitations under the License.
|
|
653
|
+
*/
|
|
654
|
+
|
|
655
|
+
package sink
|
|
656
|
+
|
|
657
|
+
import zio.blocks.streams.*
|
|
658
|
+
import zio.blocks.streams.NioSinks
|
|
659
|
+
import java.nio.ByteBuffer
|
|
660
|
+
import scala.math.pow
|
|
661
|
+
|
|
662
|
+
object SinkScientificComputingExample extends App {
|
|
663
|
+
println("=== Batching Doubles for Scientific Computing ===\n")
|
|
664
|
+
|
|
665
|
+
// Simulate raw sensor measurements that need calibration
|
|
666
|
+
println("Scenario: Streaming voltage sensor measurements with calibration curve\n")
|
|
667
|
+
|
|
668
|
+
val measurementCount = 10
|
|
669
|
+
val bufferCapacity = measurementCount * 8 // 8 bytes per Double
|
|
670
|
+
|
|
671
|
+
println(s"Processing $measurementCount measurements...\n")
|
|
672
|
+
|
|
673
|
+
// Generate raw voltage readings (0.0 to 0.09)
|
|
674
|
+
val rawVoltages = (0 until measurementCount).map(i => (i * 0.01).toDouble).toList
|
|
675
|
+
|
|
676
|
+
println("Raw measurements (voltage):")
|
|
677
|
+
rawVoltages.zipWithIndex.foreach { case (v, i) =>
|
|
678
|
+
println(f" [$i] $v%.4f V")
|
|
679
|
+
}
|
|
680
|
+
println()
|
|
681
|
+
|
|
682
|
+
// Process and calibrate measurements
|
|
683
|
+
val buffer = processAndBufferMeasurements(measurementCount, bufferCapacity)
|
|
684
|
+
|
|
685
|
+
println("After calibration (applied quadratic correction: V' = V × (1 + 0.05×V²)):\n")
|
|
686
|
+
|
|
687
|
+
// Read back and display calibrated values
|
|
688
|
+
buffer.rewind()
|
|
689
|
+
var index = 0
|
|
690
|
+
while (buffer.hasRemaining) {
|
|
691
|
+
val calibrated = buffer.getDouble()
|
|
692
|
+
println(f" [$index] $calibrated%.6f V")
|
|
693
|
+
index += 1
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
println("\n=== Pattern Use Cases ===")
|
|
697
|
+
println("This pattern is used in:")
|
|
698
|
+
println(" • Scientific instrumentation (analog-to-digital conversion)")
|
|
699
|
+
println(" • Machine learning pipelines (sensor data → training batches)")
|
|
700
|
+
println(" • Signal processing (raw signals → preprocessed data → computation)")
|
|
701
|
+
println("\nKey benefits:")
|
|
702
|
+
println(" • Zero-copy batch processing with DirectByteBuffer")
|
|
703
|
+
println(" • Efficient numerical stream transformation")
|
|
704
|
+
println(" • Memory-friendly for large datasets")
|
|
705
|
+
|
|
706
|
+
// Process and buffer measurements using NioSinks
|
|
707
|
+
def processAndBufferMeasurements(
|
|
708
|
+
measurementCount: Int,
|
|
709
|
+
bufferCapacity: Int
|
|
710
|
+
): ByteBuffer = {
|
|
711
|
+
val buffer = ByteBuffer.allocateDirect(bufferCapacity)
|
|
712
|
+
|
|
713
|
+
// Stream of raw voltage measurements (need calibration)
|
|
714
|
+
val voltages = Stream.range(0, measurementCount).map(i => (i * 0.01).toDouble)
|
|
715
|
+
|
|
716
|
+
// Apply calibration curve: quadratic correction
|
|
717
|
+
// This simulates real sensor calibration with nonlinear response
|
|
718
|
+
val calibrated = voltages.map { raw =>
|
|
719
|
+
val calibrationFactor = 1.0 + (0.05 * pow(raw, 2))
|
|
720
|
+
raw * calibrationFactor
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Write calibrated values directly to ByteBuffer using typed sink
|
|
724
|
+
// This is much faster than element-by-element byte writing
|
|
725
|
+
calibrated.run(NioSinks.fromByteBufferDouble(buffer))
|
|
726
|
+
buffer.flip()
|
|
727
|
+
buffer
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Run this example with:
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
sbt "streams-examples/runMain sink.SinkScientificComputingExample"
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
This use case is typical in scientific instrumentation, machine learning data preprocessing, and signal processing pipelines where you need to efficiently batch-process numerical streams into memory-efficient structures for downstream computation.
|
|
739
|
+
|
|
740
|
+
:::warning[Sentinel Collisions Throw — Never Silently Truncate]
|
|
741
|
+
|
|
742
|
+
These typed sinks achieve **zero-boxing performance** by using a special "sentinel" value to signal end-of-stream, rather than allocating wrapper objects or checking for `null`. This design eliminates allocations entirely, keeping the read loop a **single primitive comparison per element**. This loop shape is a deliberate, protected performance choice (see the repository's `AGENTS.md`, "Sentinel performance policy"): no per-element flag checks, rawbits conversions, boxing, or extra branches are permitted in it.
|
|
743
|
+
|
|
744
|
+
A natural question: what happens if the stream *contains* the sentinel value (e.g. a `Long.MaxValue` element streamed into `NioSinks.fromByteBufferLong`)? The sink **throws `IllegalArgumentException`** — your data is never silently dropped. Detection costs nothing on the hot path: every read records an out-of-band `lastReadWasEOF` flag on the reader, and the sink consults it **once, after the drain loop exits**, to distinguish genuine end-of-stream from a real sentinel-valued element:
|
|
745
|
+
|
|
746
|
+
```scala
|
|
747
|
+
// fromByteBufferLong - tight loop with primitives only
|
|
748
|
+
val s = Long.MaxValue
|
|
749
|
+
var v = reader.readLong(s)(using unsafeEvidence)
|
|
750
|
+
while (v != s) { // single primitive comparison per element
|
|
751
|
+
buf.putLong(v)
|
|
752
|
+
v = reader.readLong(s)(using unsafeEvidence)
|
|
753
|
+
}
|
|
754
|
+
if (!reader.lastReadWasEOF) // consulted once, post-loop: zero hot-path cost
|
|
755
|
+
throw new IllegalArgumentException("stream contains Long.MaxValue ...")
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
**Sentinels per typed sink:**
|
|
759
|
+
| Method | Input Type | Sentinel Value | Collision behavior |
|
|
760
|
+
|--------|-----------|---|---|
|
|
761
|
+
| `NioSinks.fromByteBuffer` | `Byte` | `-1` (as `Int`) | No collision possible — bytes are widened to [0, 255] |
|
|
762
|
+
| `NioSinks.fromByteBufferInt` | `Int` | `Long.MinValue` | No collision possible — outside Int range |
|
|
763
|
+
| `NioSinks.fromByteBufferLong` | `Long` | `Long.MaxValue` | Throws `IllegalArgumentException` |
|
|
764
|
+
| `NioSinks.fromByteBufferFloat` | `Float` | `Double.MaxValue` | No collision possible — outside Float range |
|
|
765
|
+
| `NioSinks.fromByteBufferDouble` | `Double` | `Double.MaxValue` | Throws `IllegalArgumentException` |
|
|
766
|
+
|
|
767
|
+
**If your data may contain the sentinel value**, use a generic sink instead — these use an out-of-band object sentinel and handle every value:
|
|
768
|
+
- `Sink.collectAll[A]` — collects into a Chunk
|
|
769
|
+
- `Sink.foreach[A](f: A => Unit)` — processes each element individually
|
|
770
|
+
- `Sink.foldLeft[A, Z](z: Z)(f: (Z, A) => Z)` — accumulates
|
|
771
|
+
- `Sink.create[E, A, Z](f: Reader[A] => Z)` — manual control
|
|
772
|
+
|
|
773
|
+
For a runnable demonstration of the guard, see the example below:
|
|
774
|
+
|
|
775
|
+
```scala title="streams-examples/src/main/scala/sink/SinkSentinelGuardExample.scala"
|
|
776
|
+
/*
|
|
777
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
778
|
+
*
|
|
779
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
780
|
+
* you may not use this file except in compliance with the License.
|
|
781
|
+
* You may obtain a copy of the License at
|
|
782
|
+
*
|
|
783
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
784
|
+
*
|
|
785
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
786
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
787
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
788
|
+
* See the License for the specific language governing permissions and
|
|
789
|
+
* limitations under the License.
|
|
790
|
+
*/
|
|
791
|
+
|
|
792
|
+
package sink
|
|
793
|
+
|
|
794
|
+
import zio.blocks.streams.*
|
|
795
|
+
import zio.blocks.streams.NioSinks
|
|
796
|
+
import java.nio.ByteBuffer
|
|
797
|
+
|
|
798
|
+
object SinkSentinelGuardExample extends App {
|
|
799
|
+
println("=== Sentinel Collisions Are Rejected Loudly (Never Silently) ===\n")
|
|
800
|
+
|
|
801
|
+
println("Context: the typed NIO sinks use a primitive sentinel (e.g. Long.MaxValue for")
|
|
802
|
+
println("fromByteBufferLong) to detect end-of-stream, keeping the drain loop a single")
|
|
803
|
+
println("primitive comparison per element — zero boxing, zero allocation. This is a")
|
|
804
|
+
println("deliberate performance choice (see AGENTS.md, Sentinel performance policy).")
|
|
805
|
+
println("If your stream contains the sentinel value itself, the sink does NOT silently")
|
|
806
|
+
println("truncate: it detects the collision at zero hot-path cost (one out-of-band EOF")
|
|
807
|
+
println("flag check after the loop exits) and throws IllegalArgumentException.\n")
|
|
808
|
+
|
|
809
|
+
// Example 1: normal data drains at full speed
|
|
810
|
+
println("Test 1: Stream without sentinel values drains completely")
|
|
811
|
+
println("-" * 60)
|
|
812
|
+
|
|
813
|
+
val safeData = List(100L, 200L, 300L, 400L, 500L)
|
|
814
|
+
val buffer1 = ByteBuffer.allocate(safeData.length * 8)
|
|
815
|
+
Stream.fromIterable(safeData).run(NioSinks.fromByteBufferLong(buffer1))
|
|
816
|
+
buffer1.flip()
|
|
817
|
+
|
|
818
|
+
var count1 = 0
|
|
819
|
+
while (buffer1.hasRemaining) {
|
|
820
|
+
println(f" [$count1] ${buffer1.getLong()}")
|
|
821
|
+
count1 += 1
|
|
822
|
+
}
|
|
823
|
+
println(f"\n✓ All ${count1} values written\n")
|
|
824
|
+
|
|
825
|
+
// Example 2: a sentinel-valued element is rejected with a clear error
|
|
826
|
+
println("Test 2: Stream containing Long.MaxValue is rejected, not truncated")
|
|
827
|
+
println("-" * 60)
|
|
828
|
+
|
|
829
|
+
val riskyData = List(100L, 200L, Long.MaxValue, 300L, 400L)
|
|
830
|
+
println(
|
|
831
|
+
f"Stream data: ${riskyData.map(v => if (v == Long.MaxValue) "Long.MaxValue" else v.toString).mkString(", ")}\n"
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
val buffer2 = ByteBuffer.allocate(riskyData.length * 8)
|
|
835
|
+
try {
|
|
836
|
+
Stream.fromIterable(riskyData).run(NioSinks.fromByteBufferLong(buffer2))
|
|
837
|
+
println("✗ UNEXPECTED: drain completed without error")
|
|
838
|
+
} catch {
|
|
839
|
+
case e: IllegalArgumentException =>
|
|
840
|
+
println(s"✓ Rejected loudly: ${e.getMessage}")
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Recommendations
|
|
844
|
+
println("\n=== Recommendations ===")
|
|
845
|
+
println("1. If your data might contain the sentinel value (Long.MaxValue for the Long")
|
|
846
|
+
println(" sink, Double.MaxValue for the Double sink):")
|
|
847
|
+
println(" → Use a generic sink (Sink.collectAll, Sink.foreach, Sink.foldLeft) — these")
|
|
848
|
+
println(" use an out-of-band object sentinel and handle every value")
|
|
849
|
+
println("2. Otherwise the typed sinks are maximally fast: a single primitive comparison")
|
|
850
|
+
println(" per element, zero boxing, zero allocation")
|
|
851
|
+
println("3. Either way, data is never silently dropped — a collision throws")
|
|
852
|
+
println()
|
|
853
|
+
println("Sentinels per typed sink:")
|
|
854
|
+
println(" → fromByteBufferInt: sentinel = Long.MinValue (outside Int range — no collision possible)")
|
|
855
|
+
println(" → fromByteBufferLong: sentinel = Long.MaxValue (collision throws)")
|
|
856
|
+
println(" → fromByteBufferFloat: sentinel = Double.MaxValue (outside Float range — no collision possible)")
|
|
857
|
+
println(" → fromByteBufferDouble: sentinel = Double.MaxValue (collision throws)")
|
|
858
|
+
}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
Run it with:
|
|
863
|
+
|
|
864
|
+
```bash
|
|
865
|
+
sbt "streams-examples/runMain sink.SinkSentinelGuardExample"
|
|
866
|
+
```
|
|
867
|
+
:::
|
|
868
|
+
|
|
869
|
+
You might ask: **Why not use a sentinel object like generic sinks do, instead of primitive values?** The answer reveals a fundamental performance trade-off.
|
|
870
|
+
|
|
871
|
+
Generic sinks use object sentinels to signal end-of-stream:
|
|
872
|
+
|
|
873
|
+
```scala
|
|
874
|
+
// Sink.collectAll - uses object reference for end-of-stream
|
|
875
|
+
def loop(v: Any): Unit =
|
|
876
|
+
if (v.asInstanceOf[AnyRef] ne EndOfStream) {
|
|
877
|
+
b += v.asInstanceOf[A]
|
|
878
|
+
loop(reader.read(EndOfStream))
|
|
879
|
+
}
|
|
880
|
+
val firstValue = reader.read(EndOfStream) // EndOfStream is an object
|
|
881
|
+
loop(firstValue)
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
**Performance Impact:**
|
|
885
|
+
- **Typed sinks:** Direct primitive comparison, zero allocations, tight loop optimizable by JVM
|
|
886
|
+
- **Generic sinks:** Object casting, reference equality check, one `EndOfStream` object per stream
|
|
887
|
+
|
|
888
|
+
For a stream processing **millions of elements**, the typed sink approach has measurably better performance because:
|
|
889
|
+
1. No casting overhead per iteration
|
|
890
|
+
2. Primitive values are faster than object references
|
|
891
|
+
3. JIT compiler can better optimize tight primitive loops
|
|
892
|
+
4. Zero per-element allocation pressure
|
|
893
|
+
|
|
894
|
+
Neither approach silently drops data: the generic sinks use a reference-unique object that no stream element can equal, and the typed sinks detect a value/sentinel collision via the out-of-band EOF flag (consulted once, post-loop) and throw rather than truncate.
|
|
895
|
+
|
|
896
|
+
### From Channel Sink
|
|
897
|
+
|
|
898
|
+
The **`Sink.fromChannel`** constructor performs buffered writes to a `WritableByteChannel` (e.g., a network socket or file channel). This is the general-purpose NIO sink: it accumulates bytes in an internal buffer of size `bufSize` (default 8192), flushes when the buffer is full, and flushes again at end-of-stream.
|
|
899
|
+
|
|
900
|
+
It handles `IOException` as a typed error, so failures surface as `Left(IOException)` from `Stream.run`. Use this for network I/O or when you can't pre-allocate a buffer. The channel I/O is blocking—NIO's non-blocking advantage comes when using selectors across many channels, which this sink does not expose.
|
|
901
|
+
|
|
902
|
+
Suppose you're collecting metrics from thousands of sensors (temperature, pressure, timestamps) and need to write them to a file efficiently. Using `NioSinks.fromChannel` with a file's `WritableByteChannel` gives you automatic buffering and eliminates manual position management.
|
|
903
|
+
|
|
904
|
+
Here is the complete example:
|
|
905
|
+
|
|
906
|
+
```scala title="streams-examples/src/main/scala/sink/SinkTelemetryExample.scala"
|
|
907
|
+
/*
|
|
908
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
909
|
+
*
|
|
910
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
911
|
+
* you may not use this file except in compliance with the License.
|
|
912
|
+
* You may obtain a copy of the License at
|
|
913
|
+
*
|
|
914
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
915
|
+
*
|
|
916
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
917
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
918
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
919
|
+
* See the License for the specific language governing permissions and
|
|
920
|
+
* limitations under the License.
|
|
921
|
+
*/
|
|
922
|
+
|
|
923
|
+
package sink
|
|
924
|
+
|
|
925
|
+
import zio.blocks.streams.*
|
|
926
|
+
import zio.blocks.streams.NioSinks
|
|
927
|
+
import java.io.RandomAccessFile
|
|
928
|
+
import java.nio.file.Files
|
|
929
|
+
import scala.util.Using
|
|
930
|
+
|
|
931
|
+
object SinkTelemetryExample extends App {
|
|
932
|
+
println("=== Streaming Telemetry to File Channel ===\n")
|
|
933
|
+
|
|
934
|
+
// Simulated sensor readings (timestamp, temperature)
|
|
935
|
+
case class SensorReading(timestamp: Long, temperature: Double) {
|
|
936
|
+
override def toString: String = f"[$timestamp] $temperature%.2f°C"
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Generate mock sensor data
|
|
940
|
+
val sensorReadings = List(
|
|
941
|
+
SensorReading(1000L, 22.5),
|
|
942
|
+
SensorReading(1100L, 23.1),
|
|
943
|
+
SensorReading(1200L, 22.8),
|
|
944
|
+
SensorReading(1300L, 23.4),
|
|
945
|
+
SensorReading(1400L, 24.0)
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
println("Sensor readings to write:")
|
|
949
|
+
sensorReadings.foreach(r => println(s" $r"))
|
|
950
|
+
println()
|
|
951
|
+
|
|
952
|
+
// Write telemetry to file using NioSinks.fromChannel
|
|
953
|
+
val tempFile = Files.createTempFile("telemetry", ".bin")
|
|
954
|
+
val filePath = tempFile.toString
|
|
955
|
+
|
|
956
|
+
println(s"Writing to $filePath...")
|
|
957
|
+
Using(new RandomAccessFile(filePath, "rw")) { file =>
|
|
958
|
+
val channel = file.getChannel
|
|
959
|
+
channel.truncate(0) // Clear file
|
|
960
|
+
|
|
961
|
+
// Serialize readings into a single buffer: 8 bytes timestamp + 8 bytes temperature per reading
|
|
962
|
+
val buffer = java.nio.ByteBuffer.allocate(sensorReadings.length * 16)
|
|
963
|
+
sensorReadings.foreach { reading =>
|
|
964
|
+
buffer.putLong(reading.timestamp)
|
|
965
|
+
buffer.putDouble(reading.temperature)
|
|
966
|
+
}
|
|
967
|
+
buffer.flip()
|
|
968
|
+
|
|
969
|
+
// Write all bytes to file with internal buffering (8KB chunks)
|
|
970
|
+
val bytes = buffer.array()
|
|
971
|
+
val byteStream = Stream.fromChunk(zio.blocks.chunk.Chunk.fromArray(bytes))
|
|
972
|
+
byteStream.run(NioSinks.fromChannel(channel, bufSize = 8192))
|
|
973
|
+
|
|
974
|
+
println(s"✓ Wrote ${file.length()} bytes to disk")
|
|
975
|
+
}.get
|
|
976
|
+
|
|
977
|
+
// Read back and verify
|
|
978
|
+
println("\nVerifying written data:")
|
|
979
|
+
Using(new RandomAccessFile(filePath, "r")) { file =>
|
|
980
|
+
val buf = java.nio.ByteBuffer.allocate((8 + 8) * sensorReadings.length)
|
|
981
|
+
file.getChannel.read(buf)
|
|
982
|
+
buf.rewind()
|
|
983
|
+
|
|
984
|
+
var count = 0
|
|
985
|
+
while (buf.remaining() >= 16) {
|
|
986
|
+
val timestamp = buf.getLong()
|
|
987
|
+
val temperature = buf.getDouble()
|
|
988
|
+
println(f" [$timestamp] $temperature%.2f°C")
|
|
989
|
+
count += 1
|
|
990
|
+
}
|
|
991
|
+
println(s"✓ Read back $count sensor readings")
|
|
992
|
+
}.get
|
|
993
|
+
|
|
994
|
+
println("\n=== Pattern Use Cases ===")
|
|
995
|
+
println("This pattern is used in:")
|
|
996
|
+
println(" • IoT telemetry platforms (time-series databases)")
|
|
997
|
+
println(" • High-throughput logging systems")
|
|
998
|
+
println(" • Sensor data aggregation pipelines")
|
|
999
|
+
println("\nKey benefits:")
|
|
1000
|
+
println(" • Automatic buffering eliminates manual position management")
|
|
1001
|
+
println(" • Integrated with Stream composition (no boilerplate)")
|
|
1002
|
+
println(" • Type-safe error handling (IOException as Sink error type)")
|
|
1003
|
+
|
|
1004
|
+
Files.delete(tempFile)
|
|
1005
|
+
}
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
Run it with:
|
|
1010
|
+
|
|
1011
|
+
```bash
|
|
1012
|
+
sbt "streams-examples/runMain sink.SinkTelemetryExample"
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
This pattern is common in high-throughput logging systems, time-series databases, and IoT platforms where you need to write streams of telemetry data to persistent storage without blocking or allocating excessively.
|
|
1016
|
+
|
|
1017
|
+
## Running the Examples
|
|
1018
|
+
|
|
1019
|
+
All code from this guide is available as runnable examples in the `streams-examples` module.
|
|
1020
|
+
|
|
1021
|
+
Start by cloning the repository and navigating to the project:
|
|
1022
|
+
|
|
1023
|
+
```bash
|
|
1024
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
1025
|
+
cd zio-blocks
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
Run individual examples with sbt:
|
|
1029
|
+
|
|
1030
|
+
### Basic Usage
|
|
1031
|
+
|
|
1032
|
+
This example demonstrates the most commonly used built-in sinks: `Sink.drain`, `Sink.count`, `Sink.collectAll`, `Sink.head`, `Sink.last`, and `Sink.take`:
|
|
1033
|
+
|
|
1034
|
+
```scala title="streams-examples/src/main/scala/sink/SinkBasicUsageExample.scala"
|
|
1035
|
+
/*
|
|
1036
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1037
|
+
*
|
|
1038
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1039
|
+
* you may not use this file except in compliance with the License.
|
|
1040
|
+
* You may obtain a copy of the License at
|
|
1041
|
+
*
|
|
1042
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1043
|
+
*
|
|
1044
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1045
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1046
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1047
|
+
* See the License for the specific language governing permissions and
|
|
1048
|
+
* limitations under the License.
|
|
1049
|
+
*/
|
|
1050
|
+
|
|
1051
|
+
package sink
|
|
1052
|
+
|
|
1053
|
+
import zio.blocks.streams.*
|
|
1054
|
+
import zio.sbt.ExprEval.show
|
|
1055
|
+
|
|
1056
|
+
object SinkBasicUsageExample extends App {
|
|
1057
|
+
println("=== Sink Basic Usage ===\n")
|
|
1058
|
+
|
|
1059
|
+
val data = Stream(1, 2, 3, 4, 5)
|
|
1060
|
+
|
|
1061
|
+
// 1. Sink.drain — discard all elements
|
|
1062
|
+
println("1. Sink.drain — discard all elements:")
|
|
1063
|
+
show(data.run(Sink.drain))
|
|
1064
|
+
|
|
1065
|
+
// 2. Sink.count — count elements
|
|
1066
|
+
println("\n2. Sink.count — count elements:")
|
|
1067
|
+
show(data.run(Sink.count))
|
|
1068
|
+
|
|
1069
|
+
// 3. Sink.collectAll — collect into Chunk
|
|
1070
|
+
println("\n3. Sink.collectAll — collect into Chunk:")
|
|
1071
|
+
show(data.run(Sink.collectAll[Int]))
|
|
1072
|
+
|
|
1073
|
+
// 4. Sink.head — first element
|
|
1074
|
+
println("\n4. Sink.head — first element:")
|
|
1075
|
+
show(data.run(Sink.head[Int]))
|
|
1076
|
+
|
|
1077
|
+
println("\n Sink.head on empty stream:")
|
|
1078
|
+
show(Stream.empty.run(Sink.head[Int]))
|
|
1079
|
+
|
|
1080
|
+
// 5. Sink.last — last element
|
|
1081
|
+
println("\n5. Sink.last — last element:")
|
|
1082
|
+
show(data.run(Sink.last[Int]))
|
|
1083
|
+
|
|
1084
|
+
// 6. Sink.take — first n elements
|
|
1085
|
+
println("\n6. Sink.take — first n elements:")
|
|
1086
|
+
show(data.run(Sink.take(3)))
|
|
1087
|
+
|
|
1088
|
+
// 7. take on a large stream (short-circuits)
|
|
1089
|
+
println("\n7. Sink.take short-circuits (only reads 3 of 1000):")
|
|
1090
|
+
show(Stream.range(0, 1000).run(Sink.take(3)))
|
|
1091
|
+
|
|
1092
|
+
// 8. Combining stream operations with sinks
|
|
1093
|
+
println("\n8. Combining stream operations with explicit sinks:")
|
|
1094
|
+
val result = Stream(1, 2, 3, 4, 5)
|
|
1095
|
+
.filter(_ % 2 == 0)
|
|
1096
|
+
.run(Sink.collectAll[Int])
|
|
1097
|
+
show(result)
|
|
1098
|
+
|
|
1099
|
+
// 9. Equivalence with convenience methods
|
|
1100
|
+
println("\n9. Stream convenience methods delegate to sinks:")
|
|
1101
|
+
show(data.runCollect == data.run(Sink.collectAll[Int]))
|
|
1102
|
+
show(data.count == data.run(Sink.count))
|
|
1103
|
+
}
|
|
1104
|
+
```
|
|
1105
|
+
|
|
1106
|
+
Run this example with:
|
|
1107
|
+
|
|
1108
|
+
```bash
|
|
1109
|
+
sbt "streams-examples/runMain sink.SinkBasicUsageExample"
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
### Aggregation and Search
|
|
1113
|
+
|
|
1114
|
+
This example shows aggregation sinks (`Sink.foldLeft`, `Sink.sumInt`, `Sink.sumDouble`) and search sinks (`Sink.exists`, `Sink.forall`, `Sink.find`, `Sink.foreach`):
|
|
1115
|
+
|
|
1116
|
+
```scala title="streams-examples/src/main/scala/sink/SinkAggregationExample.scala"
|
|
1117
|
+
/*
|
|
1118
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1119
|
+
*
|
|
1120
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1121
|
+
* you may not use this file except in compliance with the License.
|
|
1122
|
+
* You may obtain a copy of the License at
|
|
1123
|
+
*
|
|
1124
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1125
|
+
*
|
|
1126
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1127
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1128
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1129
|
+
* See the License for the specific language governing permissions and
|
|
1130
|
+
* limitations under the License.
|
|
1131
|
+
*/
|
|
1132
|
+
|
|
1133
|
+
package sink
|
|
1134
|
+
|
|
1135
|
+
import zio.blocks.streams.*
|
|
1136
|
+
import zio.sbt.ExprEval.show
|
|
1137
|
+
|
|
1138
|
+
object SinkAggregationExample extends App {
|
|
1139
|
+
println("=== Sink Aggregation and Search ===\n")
|
|
1140
|
+
|
|
1141
|
+
// 1. foldLeft — general accumulation
|
|
1142
|
+
println("1. Sink.foldLeft — general accumulation:")
|
|
1143
|
+
val sum = Stream(1, 2, 3, 4, 5).run(Sink.foldLeft(0)(_ + _))
|
|
1144
|
+
show(sum)
|
|
1145
|
+
|
|
1146
|
+
println("\n foldLeft with string concatenation:")
|
|
1147
|
+
val concat = Stream("a", "b", "c").run(Sink.foldLeft("")(_ + _))
|
|
1148
|
+
show(concat)
|
|
1149
|
+
// 2. sumInt — typed numeric sum
|
|
1150
|
+
println("\n2. Sink.sumInt — returns Long to avoid overflow:")
|
|
1151
|
+
val intSum = Stream(1, 2, 3, 4, 5).run(Sink.sumInt)
|
|
1152
|
+
show(intSum)
|
|
1153
|
+
|
|
1154
|
+
// 3. sumDouble — typed floating point sum
|
|
1155
|
+
println("\n3. Sink.sumDouble:")
|
|
1156
|
+
val doubleSum = Stream(1.5, 2.5, 3.0).run(Sink.sumDouble)
|
|
1157
|
+
show(doubleSum)
|
|
1158
|
+
|
|
1159
|
+
// 4. exists — short-circuits on first match
|
|
1160
|
+
println("\n4. Sink.exists — short-circuits on first match:")
|
|
1161
|
+
val hasNegative = Stream(1, 2, -3, 4).run(Sink.exists[Int](_ < 0))
|
|
1162
|
+
show(hasNegative)
|
|
1163
|
+
|
|
1164
|
+
val noNegative = Stream(1, 2, 3, 4).run(Sink.exists[Int](_ < 0))
|
|
1165
|
+
show(noNegative)
|
|
1166
|
+
|
|
1167
|
+
// 5. forall — all elements must match
|
|
1168
|
+
println("\n5. Sink.forall — all elements must match:")
|
|
1169
|
+
val allPositive = Stream(1, 2, 3).run(Sink.forall[Int](_ > 0))
|
|
1170
|
+
show(allPositive)
|
|
1171
|
+
|
|
1172
|
+
val notAllPositive = Stream(1, -2, 3).run(Sink.forall[Int](_ > 0))
|
|
1173
|
+
show(notAllPositive)
|
|
1174
|
+
|
|
1175
|
+
// 6. find — first element matching predicate
|
|
1176
|
+
println("\n6. Sink.find — first matching element:")
|
|
1177
|
+
val firstEven = Stream(1, 3, 4, 6, 8).run(Sink.find[Int](_ % 2 == 0))
|
|
1178
|
+
show(firstEven)
|
|
1179
|
+
|
|
1180
|
+
val noMatch = Stream(1, 3, 5, 7).run(Sink.find[Int](_ % 2 == 0))
|
|
1181
|
+
show(noMatch)
|
|
1182
|
+
|
|
1183
|
+
// 7. foreach — side effects
|
|
1184
|
+
println("\n7. Sink.foreach — apply side effects:")
|
|
1185
|
+
val items = scala.collection.mutable.Buffer[String]()
|
|
1186
|
+
val result = Stream("x", "y", "z").run(Sink.foreach[String](s => items += s))
|
|
1187
|
+
show(result)
|
|
1188
|
+
show(items.toList)
|
|
1189
|
+
|
|
1190
|
+
// 8. Complex aggregation: combine foldLeft with map
|
|
1191
|
+
println("\n8. Complex aggregation — average via foldLeft + map:")
|
|
1192
|
+
val average = Sink
|
|
1193
|
+
.foldLeft[Int, (Int, Int)]((0, 0)) { case ((sum, count), x) =>
|
|
1194
|
+
(sum + x, count + 1)
|
|
1195
|
+
}
|
|
1196
|
+
.map { case (sum, count) =>
|
|
1197
|
+
if (count == 0) 0.0 else sum.toDouble / count
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
val avg = Stream(10, 20, 30, 40).run(average)
|
|
1201
|
+
show(avg)
|
|
1202
|
+
}
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
Run this example with:
|
|
1206
|
+
|
|
1207
|
+
```bash
|
|
1208
|
+
sbt "streams-examples/runMain sink.SinkAggregationExample"
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
### Transformations and Composition
|
|
1212
|
+
|
|
1213
|
+
This example demonstrates `Sink#contramap`, `Sink#map`, `Sink#mapError`, `Sink.fail`, `Sink.create`, and `Pipeline#andThenSink`:
|
|
1214
|
+
|
|
1215
|
+
```scala title="streams-examples/src/main/scala/sink/SinkTransformationExample.scala"
|
|
1216
|
+
/*
|
|
1217
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1218
|
+
*
|
|
1219
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1220
|
+
* you may not use this file except in compliance with the License.
|
|
1221
|
+
* You may obtain a copy of the License at
|
|
1222
|
+
*
|
|
1223
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1224
|
+
*
|
|
1225
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1226
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1227
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1228
|
+
* See the License for the specific language governing permissions and
|
|
1229
|
+
* limitations under the License.
|
|
1230
|
+
*/
|
|
1231
|
+
|
|
1232
|
+
package sink
|
|
1233
|
+
|
|
1234
|
+
import zio.blocks.streams.*
|
|
1235
|
+
import zio.sbt.ExprEval.show
|
|
1236
|
+
|
|
1237
|
+
object SinkTransformationExample extends App {
|
|
1238
|
+
println("=== Sink Transformations and Composition ===\n")
|
|
1239
|
+
|
|
1240
|
+
// 1. contramap — pre-process input
|
|
1241
|
+
println("1. Sink.contramap — pre-process input elements:")
|
|
1242
|
+
val stringLengthSum: Sink[Nothing, String, Long] =
|
|
1243
|
+
Sink.sumInt.contramap[String](_.length)
|
|
1244
|
+
|
|
1245
|
+
show(Stream("hello", "world").run(stringLengthSum))
|
|
1246
|
+
|
|
1247
|
+
// 2. contramap — change element type
|
|
1248
|
+
println("\n2. contramap to convert types:")
|
|
1249
|
+
val parseInts: Sink[Nothing, String, Long] =
|
|
1250
|
+
Sink.sumInt.contramap[String](_.toInt)
|
|
1251
|
+
|
|
1252
|
+
show(Stream("10", "20", "30").run(parseInts))
|
|
1253
|
+
|
|
1254
|
+
// 3. map — transform result
|
|
1255
|
+
println("\n3. Sink.map — transform the result:")
|
|
1256
|
+
val countFormatted: Sink[Nothing, Any, String] =
|
|
1257
|
+
Sink.count.map(n => s"Processed $n elements")
|
|
1258
|
+
|
|
1259
|
+
show(Stream(1, 2, 3).run(countFormatted))
|
|
1260
|
+
|
|
1261
|
+
// 4. Chaining contramap + map
|
|
1262
|
+
println("\n4. Chaining contramap + map:")
|
|
1263
|
+
val pipeline = Sink.sumInt
|
|
1264
|
+
.contramap[String](_.length)
|
|
1265
|
+
.map(total => s"Total chars: $total")
|
|
1266
|
+
|
|
1267
|
+
show(Stream("hi", "hello").run(pipeline))
|
|
1268
|
+
|
|
1269
|
+
// 5. mapError — transform error channel
|
|
1270
|
+
println("\n5. Sink.mapError — transform errors:")
|
|
1271
|
+
|
|
1272
|
+
sealed trait AppError
|
|
1273
|
+
case class ParseError(msg: String) extends AppError
|
|
1274
|
+
|
|
1275
|
+
val failingSink = Sink.fail("raw error").mapError[AppError](msg => ParseError(msg))
|
|
1276
|
+
show(Stream(1).run(failingSink))
|
|
1277
|
+
|
|
1278
|
+
// 6. fail — immediately fail
|
|
1279
|
+
println("\n6. Sink.fail — immediate failure:")
|
|
1280
|
+
show(Stream(1, 2, 3).run(Sink.fail("error")))
|
|
1281
|
+
|
|
1282
|
+
// 7. Pipeline.andThenSink integration
|
|
1283
|
+
println("\n7. Pipeline.andThenSink — pipeline pre-processes before sink:")
|
|
1284
|
+
val cleanAndCollect =
|
|
1285
|
+
Pipeline
|
|
1286
|
+
.map[String, String](_.trim.toLowerCase)
|
|
1287
|
+
.andThenSink(Sink.collectAll[String])
|
|
1288
|
+
|
|
1289
|
+
show(Stream(" Hello ", " WORLD ").run(cleanAndCollect))
|
|
1290
|
+
|
|
1291
|
+
// 8. Equivalence: via + run == andThenSink + run
|
|
1292
|
+
println("\n8. Equivalence law: via + run == andThenSink + run:")
|
|
1293
|
+
val pipe = Pipeline.filter[Int](_ > 2).andThen(Pipeline.map[Int, Int](_ * 10))
|
|
1294
|
+
val source = Stream(1, 2, 3, 4, 5)
|
|
1295
|
+
|
|
1296
|
+
val viaResult = source.via(pipe).run(Sink.collectAll[Int])
|
|
1297
|
+
val sinkResult = source.run(pipe.andThenSink(Sink.collectAll[Int]))
|
|
1298
|
+
show(viaResult)
|
|
1299
|
+
show(sinkResult)
|
|
1300
|
+
|
|
1301
|
+
// 9. Composing multiple transformations into a reusable sink
|
|
1302
|
+
println("\n9. Reusable composed sink:")
|
|
1303
|
+
case class Metric(name: String, value: Double)
|
|
1304
|
+
|
|
1305
|
+
val metricSumSink: Sink[Nothing, Metric, Double] =
|
|
1306
|
+
Sink.foldLeft(0.0)((acc, m: Metric) => acc + m.value)
|
|
1307
|
+
|
|
1308
|
+
val metrics = Stream(
|
|
1309
|
+
Metric("cpu", 45.0),
|
|
1310
|
+
Metric("cpu", 67.0),
|
|
1311
|
+
Metric("cpu", 23.0)
|
|
1312
|
+
)
|
|
1313
|
+
show(metrics.run(metricSumSink))
|
|
1314
|
+
|
|
1315
|
+
val metricAvgSink: Sink[Nothing, Metric, Double] =
|
|
1316
|
+
Sink
|
|
1317
|
+
.foldLeft[Metric, (Double, Int)]((0.0, 0)) { case ((sum, count), m) =>
|
|
1318
|
+
(sum + m.value, count + 1)
|
|
1319
|
+
}
|
|
1320
|
+
.map { case (sum, count) => if (count == 0) 0.0 else sum / count }
|
|
1321
|
+
|
|
1322
|
+
show(metrics.run(metricAvgSink))
|
|
1323
|
+
}
|
|
1324
|
+
```
|
|
1325
|
+
|
|
1326
|
+
Run this example with:
|
|
1327
|
+
|
|
1328
|
+
```bash
|
|
1329
|
+
sbt "streams-examples/runMain sink.SinkTransformationExample"
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
### Sentinel Guard (NIO Typed Sinks)
|
|
1333
|
+
|
|
1334
|
+
This example demonstrates that the typed NIO sinks (`NioSinks.fromByteBufferLong`, `NioSinks.fromByteBufferDouble`) reject streams containing their sentinel value (e.g., `Long.MaxValue` for `NioSinks.fromByteBufferLong`) with an `IllegalArgumentException` instead of silently truncating — detected at zero hot-path cost via the reader's out-of-band EOF flag, consulted once after the drain loop exits:
|
|
1335
|
+
|
|
1336
|
+
```scala title="streams-examples/src/main/scala/sink/SinkSentinelGuardExample.scala"
|
|
1337
|
+
/*
|
|
1338
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
1339
|
+
*
|
|
1340
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
1341
|
+
* you may not use this file except in compliance with the License.
|
|
1342
|
+
* You may obtain a copy of the License at
|
|
1343
|
+
*
|
|
1344
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
1345
|
+
*
|
|
1346
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
1347
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
1348
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
1349
|
+
* See the License for the specific language governing permissions and
|
|
1350
|
+
* limitations under the License.
|
|
1351
|
+
*/
|
|
1352
|
+
|
|
1353
|
+
package sink
|
|
1354
|
+
|
|
1355
|
+
import zio.blocks.streams.*
|
|
1356
|
+
import zio.blocks.streams.NioSinks
|
|
1357
|
+
import java.nio.ByteBuffer
|
|
1358
|
+
|
|
1359
|
+
object SinkSentinelGuardExample extends App {
|
|
1360
|
+
println("=== Sentinel Collisions Are Rejected Loudly (Never Silently) ===\n")
|
|
1361
|
+
|
|
1362
|
+
println("Context: the typed NIO sinks use a primitive sentinel (e.g. Long.MaxValue for")
|
|
1363
|
+
println("fromByteBufferLong) to detect end-of-stream, keeping the drain loop a single")
|
|
1364
|
+
println("primitive comparison per element — zero boxing, zero allocation. This is a")
|
|
1365
|
+
println("deliberate performance choice (see AGENTS.md, Sentinel performance policy).")
|
|
1366
|
+
println("If your stream contains the sentinel value itself, the sink does NOT silently")
|
|
1367
|
+
println("truncate: it detects the collision at zero hot-path cost (one out-of-band EOF")
|
|
1368
|
+
println("flag check after the loop exits) and throws IllegalArgumentException.\n")
|
|
1369
|
+
|
|
1370
|
+
// Example 1: normal data drains at full speed
|
|
1371
|
+
println("Test 1: Stream without sentinel values drains completely")
|
|
1372
|
+
println("-" * 60)
|
|
1373
|
+
|
|
1374
|
+
val safeData = List(100L, 200L, 300L, 400L, 500L)
|
|
1375
|
+
val buffer1 = ByteBuffer.allocate(safeData.length * 8)
|
|
1376
|
+
Stream.fromIterable(safeData).run(NioSinks.fromByteBufferLong(buffer1))
|
|
1377
|
+
buffer1.flip()
|
|
1378
|
+
|
|
1379
|
+
var count1 = 0
|
|
1380
|
+
while (buffer1.hasRemaining) {
|
|
1381
|
+
println(f" [$count1] ${buffer1.getLong()}")
|
|
1382
|
+
count1 += 1
|
|
1383
|
+
}
|
|
1384
|
+
println(f"\n✓ All ${count1} values written\n")
|
|
1385
|
+
|
|
1386
|
+
// Example 2: a sentinel-valued element is rejected with a clear error
|
|
1387
|
+
println("Test 2: Stream containing Long.MaxValue is rejected, not truncated")
|
|
1388
|
+
println("-" * 60)
|
|
1389
|
+
|
|
1390
|
+
val riskyData = List(100L, 200L, Long.MaxValue, 300L, 400L)
|
|
1391
|
+
println(
|
|
1392
|
+
f"Stream data: ${riskyData.map(v => if (v == Long.MaxValue) "Long.MaxValue" else v.toString).mkString(", ")}\n"
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
val buffer2 = ByteBuffer.allocate(riskyData.length * 8)
|
|
1396
|
+
try {
|
|
1397
|
+
Stream.fromIterable(riskyData).run(NioSinks.fromByteBufferLong(buffer2))
|
|
1398
|
+
println("✗ UNEXPECTED: drain completed without error")
|
|
1399
|
+
} catch {
|
|
1400
|
+
case e: IllegalArgumentException =>
|
|
1401
|
+
println(s"✓ Rejected loudly: ${e.getMessage}")
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
// Recommendations
|
|
1405
|
+
println("\n=== Recommendations ===")
|
|
1406
|
+
println("1. If your data might contain the sentinel value (Long.MaxValue for the Long")
|
|
1407
|
+
println(" sink, Double.MaxValue for the Double sink):")
|
|
1408
|
+
println(" → Use a generic sink (Sink.collectAll, Sink.foreach, Sink.foldLeft) — these")
|
|
1409
|
+
println(" use an out-of-band object sentinel and handle every value")
|
|
1410
|
+
println("2. Otherwise the typed sinks are maximally fast: a single primitive comparison")
|
|
1411
|
+
println(" per element, zero boxing, zero allocation")
|
|
1412
|
+
println("3. Either way, data is never silently dropped — a collision throws")
|
|
1413
|
+
println()
|
|
1414
|
+
println("Sentinels per typed sink:")
|
|
1415
|
+
println(" → fromByteBufferInt: sentinel = Long.MinValue (outside Int range — no collision possible)")
|
|
1416
|
+
println(" → fromByteBufferLong: sentinel = Long.MaxValue (collision throws)")
|
|
1417
|
+
println(" → fromByteBufferFloat: sentinel = Double.MaxValue (outside Float range — no collision possible)")
|
|
1418
|
+
println(" → fromByteBufferDouble: sentinel = Double.MaxValue (collision throws)")
|
|
1419
|
+
}
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
Run it with this command:
|
|
1423
|
+
|
|
1424
|
+
```bash
|
|
1425
|
+
sbt "streams-examples/runMain sink.SinkSentinelGuardExample"
|
|
1426
|
+
```
|