@zio.dev/zio-blocks 0.0.33 → 0.0.55
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/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -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 +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- 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 +1499 -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/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -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 +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- 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 +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -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} +5 -5
- 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} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -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} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/schema/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- 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 +1032 -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 +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -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 +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- 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,1404 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: sink
|
|
3
|
+
title: "Sink"
|
|
4
|
+
sidebar_label: "Sink"
|
|
5
|
+
description: "The stream consumer: its dual synchronous and asynchronous drains, the three construction factories, and the async aggregation and transformation combinators."
|
|
6
|
+
keywords:
|
|
7
|
+
- "Stream Consumer"
|
|
8
|
+
- "Dual Drain"
|
|
9
|
+
- "Async Sinks"
|
|
10
|
+
- "Custom Sink Factories"
|
|
11
|
+
- "Sink"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
`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`. Every sink has synchronous and asynchronous drain paths: use cross-platform `Stream.runAsync`, or the JVM-only blocking `Stream.run`.
|
|
15
|
+
|
|
16
|
+
`Sink`:
|
|
17
|
+
- Is covariant in `E` (error) and `Z` (result) — these are outputs
|
|
18
|
+
- Is contravariant in `A` (input) — a `Sink[_, Any, _]` accepts any element type
|
|
19
|
+
- Dispatches on the reader's physical primitive lane, so element pulls avoid boxing
|
|
20
|
+
- Provides `Sink#contramap`, `Sink#map`, and `Sink#mapError` for composable transformations
|
|
21
|
+
|
|
22
|
+
Built-in sinks implement both drain paths. Asynchronous constructors include `existsAsync`, `findAsync`, `forallAsync`, `foreachAsync`, and `foldLeftAsync`; callbacks are sequential and back-pressured. The result/error combinators `contramapAsync`, `mapAsync`, and `mapErrorAsync` likewise select the native async drain when used by `runAsync`. On the JVM, driving such a sink through a plain blocking terminal blocks while awaiting its async drain; Scala.js has no blocking terminal.
|
|
23
|
+
|
|
24
|
+
Here is the structural shape of the `Sink` type:
|
|
25
|
+
|
|
26
|
+
```scala
|
|
27
|
+
abstract class Sink[+E, -A, +Z] {
|
|
28
|
+
def contramap[A0 <: A, A2](g: A2 => A0)(implicit jtA0: JvmType.Infer[A0]): Sink[E, A2, Z]
|
|
29
|
+
def contramapAsync[A0 <: A, A2](g: A2 => Async[A0])(implicit jtA0: JvmType.Infer[A0]): Sink[E, A2, Z]
|
|
30
|
+
def map[Z2](f: Z => Z2): Sink[E, A, Z2]
|
|
31
|
+
def mapAsync[Z2](f: Z => Async[Z2]): Sink[E, A, Z2]
|
|
32
|
+
def mapError[E2](f: E => E2)(implicit isNothing: Sink.IsNothing[E]): Sink[E2, A, Z]
|
|
33
|
+
def mapErrorAsync[E2](f: E => Async[E2])(implicit isNothing: Sink.IsNothing[E]): Sink[E2, A, Z]
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Overview
|
|
38
|
+
|
|
39
|
+
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*:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
|
|
43
|
+
│ Stream[E, A] │ ──→ │ Pipeline[A, B] │ ──→ │ Sink[E, B, Z]│
|
|
44
|
+
└──────────────┘ └──────────────────┘ └──────────────┘
|
|
45
|
+
│
|
|
46
|
+
┌───────▼──────┐
|
|
47
|
+
│ Either[E, Z] │
|
|
48
|
+
└──────────────┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
When you call `stream.run(sink)`:
|
|
52
|
+
1. The stream compiles into a `Reader` (a low-level pull-based source)
|
|
53
|
+
2. The sink's internal `Sink#drain` method pulls elements in a tight loop until end-of-stream
|
|
54
|
+
3. On success, the result wraps in `Right(z)`
|
|
55
|
+
4. Typed errors (`E`) surface as `Left(e)`, while untyped defects propagate as exceptions
|
|
56
|
+
5. The reader's `close()` runs after the drain whether it succeeded or threw, and a failure from `close()` is attached to the drain's own error rather than replacing it, ensuring resource safety
|
|
57
|
+
|
|
58
|
+
### Physical Input Lanes and Ownership
|
|
59
|
+
|
|
60
|
+
A sink consumes either kind of reader. `Reader` is not one type with a mode flag: it splits into `Reader.SyncReader[A]`, whose pulls return values, and `Reader.AsyncReader[A]`, whose pulls return [`Async`](../../async.md) values. [The reader union](../primitives/reader.md#the-reader-union) describes the split from the reader's side. From the sink's side, the consequence is that every sink is drained through one of two entry points, and the terminal decides which — the next section covers that choice.
|
|
61
|
+
|
|
62
|
+
Whichever kind arrives, a sink discovers its input representation from the materialized reader's `jvmType`, not from the sink's contravariant static input type. Generic sinks dispatch once per drain and then pull `Boolean`, `Byte`, `Char`, `Short`, `Int`, `Long`, `Float`, and `Double` through library-internal physical pulls. The synchronous drain uses `readBooleanPhysical`, `readBytePhysical`, `readCharPhysical`, `readShortPhysical`, `readIntPhysical`, `readLongsPhysical`, `readFloatPhysical`, and `readDoublesPhysical`, respectively; the asynchronous drain differs on two lanes, pulling `Byte` through `readBytesPhysical` and `Float` through `readFloatsPhysical`. These are the internal counterparts of the public `readBoolean`, `readByte`, `readChar`, `readShort`, `readInt`, `readLongs`, `readFloat`, and `readDoubles`, which a generic sink cannot call because all but `readByte` demand `Elem <:< X` evidence it has no way to supply. Reference inputs use generic `read`. In particular, the four small primitive lanes do not share the `Int` pull.
|
|
63
|
+
|
|
64
|
+
`Long` and `Double` use one-element primitive arrays with `readLongsPhysical(scratch, 0, 1)` and `readDoublesPhysical(scratch, 0, 1)`, and on the asynchronous drain `Byte` and `Float` use one-element arrays in the same way. The returned count carries end-of-stream status out of band, so every `Long` value and every raw `Double` bit pattern—including NaN payloads—remains valid data. The scratch storage is allocated once per drain, not once per element.
|
|
65
|
+
|
|
66
|
+
The reader owns this physical representation. Consequently, widening a specialized stream (for example, from `Stream[Nothing, Int]` to `Stream[Nothing, AnyVal]`) does not erase its `Int` lane before it reaches a sink. A sink adapter preserves the wrapped reader's lane and forwards every exact pull method. It also preserves ownership: the run terminal owns and closes the materialized reader; a sink or sink adapter does not independently close it. External destinations supplied to I/O sinks remain caller-owned unless a constructor explicitly says otherwise.
|
|
67
|
+
|
|
68
|
+
## Consuming Asynchronous Readers
|
|
69
|
+
|
|
70
|
+
Every sink carries two drains, one per reader kind:
|
|
71
|
+
|
|
72
|
+
```scala
|
|
73
|
+
abstract class Sink[+E, -A, +Z] {
|
|
74
|
+
private[streams] def drain(reader: Reader.SyncReader[_]): Z
|
|
75
|
+
private[streams] def drain(reader: Reader.AsyncReader[_]): Async[Z]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
They are an overload on the argument type, not a runtime branch inside one method, so the selection is a static one made where the reader is known: at the terminal. Both are `private[streams]`, which means you never call either yourself. The terminal you already chose called one of them for you:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
┌────────────────────────────────────────────────────────────────────┐
|
|
83
|
+
│ Stream#run (JVM only) Stream#runAsync (JVM and JS) │
|
|
84
|
+
│ │ materializes │ materializes │
|
|
85
|
+
│ ▼ ▼ │
|
|
86
|
+
│ Reader.SyncReader[A] Reader.AsyncReader[A] │
|
|
87
|
+
│ │ is handed to │ is handed to │
|
|
88
|
+
│ ▼ ▼ │
|
|
89
|
+
│ drain(SyncReader): Z drain(AsyncReader): Async[Z] │
|
|
90
|
+
│ │ returns │ returns │
|
|
91
|
+
│ ▼ ▼ │
|
|
92
|
+
│ Either[E, Z] Async[Either[E, Z]] │
|
|
93
|
+
└────────────────────────────────────────────────────────────────────┘
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The asynchronous drain is a native one, not an adapter: `Stream#runAsync` hands the sink a `Reader.AsyncReader` and never converts it to a blocking reader first, so no thread is parked waiting for a pull. [Async Terminals](../execution-and-compatibility/async-execution.md#async-terminals) covers the terminal family that takes this path, and [The `Async[Either[E, Z]]` convention](../execution-and-compatibility/async-execution.md#the-asynceithere-z-convention) explains why the typed error stays inside the `Either`.
|
|
97
|
+
|
|
98
|
+
Every built-in sink implements both drains, so nothing on this page is available on only one path. A third member, `drainAsync(reader: Reader.SyncReader[_]): Async[Z]`, is likewise `private[streams]`; it presents a synchronous reader through the asynchronous contract by calling `reader.toAsync`, and exists for internal stages that must expose an asynchronous face over synchronous input.
|
|
99
|
+
|
|
100
|
+
### Choosing a Factory
|
|
101
|
+
|
|
102
|
+
Because a `Sink` has two abstract drains and both are `private[streams]`, code outside `zio.blocks.streams` cannot implement them — subclassing `Sink` directly is not a supported route, and the compiler will not let you complete the class. Build custom sinks from one of the three factories instead:
|
|
103
|
+
|
|
104
|
+
```scala
|
|
105
|
+
object Sink {
|
|
106
|
+
// JVM and Scala.js
|
|
107
|
+
def createAsync[E, A, Z](f: Reader.AsyncReader[A] => Async[Z]): Sink[E, A, Z]
|
|
108
|
+
def createBoth[E, A, Z](
|
|
109
|
+
sync: Reader.SyncReader[A] => Z,
|
|
110
|
+
async: Reader.AsyncReader[A] => Async[Z]
|
|
111
|
+
): Sink[E, A, Z]
|
|
112
|
+
|
|
113
|
+
// JVM only — absent from the Scala.js companion entirely
|
|
114
|
+
def create[E, A, Z](f: Reader.SyncReader[A] => Z): Sink[E, A, Z]
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Each factory fills in the drain you did not write:
|
|
119
|
+
|
|
120
|
+
| Factory | Callbacks you write | Platforms | Reach for it when |
|
|
121
|
+
|--------------------|-----------------------------------|------------------|----------------------------------------------------------------------|
|
|
122
|
+
| `Sink.createAsync` | one asynchronous | JVM and Scala.js | one implementation is enough, and awaiting per element is acceptable |
|
|
123
|
+
| `Sink.createBoth` | one synchronous, one asynchronous | JVM and Scala.js | the synchronous path can be cheaper than the asynchronous one |
|
|
124
|
+
| `Sink.create` | one synchronous | JVM only | the sink is JVM-only anyway and its callback wants a blocking reader |
|
|
125
|
+
|
|
126
|
+
`Sink.createAsync` writes the synchronous drain for you by awaiting the asynchronous one at a JVM blocking terminal. `Sink.create` writes the asynchronous drain for you by deferring the blocking callback into a cancellable effect, where cancellation closes the reader — the callback still runs inline on whichever thread drives that effect, so an asynchronous terminal driving a `Sink.create` sink blocks that thread for the duration of the callback. `Sink.createBoth` writes neither: it takes two independent callbacks and the terminal runs exactly one, so the two must agree on how much input they consume and what they produce — nothing compares their results.
|
|
127
|
+
|
|
128
|
+
:::warning[`Sink.create` does not exist on Scala.js]
|
|
129
|
+
It is declared in the JVM copy of `SinkCompanionPlatformSpecific` and simply absent from the Scala.js copy, so cross-built code that calls it fails to compile for the JS target rather than failing at runtime. [`Sink.create` and Custom Sinks](../execution-and-compatibility/platform-differences.md#sinkcreate-and-custom-sinks) states the platform rule.
|
|
130
|
+
:::
|
|
131
|
+
|
|
132
|
+
[Custom sink factories](#custom-sink-factories) shows a worked callback against the reader protocol.
|
|
133
|
+
|
|
134
|
+
## Predefined Sinks
|
|
135
|
+
|
|
136
|
+
These are value sinks (no factory arguments). They work on any element type.
|
|
137
|
+
|
|
138
|
+
### `Sink.drain` — Discard All Elements
|
|
139
|
+
|
|
140
|
+
Consumes every element and discards them. Returns `Unit`:
|
|
141
|
+
|
|
142
|
+
```scala
|
|
143
|
+
object Sink {
|
|
144
|
+
val drain: Sink[Nothing, Any, Unit]
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Use `Sink.drain` when you only care about side effects (e.g., via `Stream#tapEach`) and not the elements themselves:
|
|
149
|
+
|
|
150
|
+
```scala
|
|
151
|
+
import zio.blocks.streams._
|
|
152
|
+
import scala.collection.mutable.Buffer
|
|
153
|
+
|
|
154
|
+
val log = Buffer[String]()
|
|
155
|
+
// log: Buffer[String] = ArrayBuffer(
|
|
156
|
+
// "Processing: 1",
|
|
157
|
+
// "Processing: 2",
|
|
158
|
+
// "Processing: 3"
|
|
159
|
+
// )
|
|
160
|
+
val result = Stream(1, 2, 3)
|
|
161
|
+
.tapEach(x => log += s"Processing: $x")
|
|
162
|
+
.run(Sink.drain)
|
|
163
|
+
// result: Either[Nothing, Unit] = Right(())
|
|
164
|
+
// result is Right(())
|
|
165
|
+
// log contains: ["Processing: 1", "Processing: 2", "Processing: 3"]
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### `Sink.count` — Count Elements
|
|
169
|
+
|
|
170
|
+
Counts the total number of elements consumed. Returns `Long`:
|
|
171
|
+
|
|
172
|
+
```scala
|
|
173
|
+
object Sink {
|
|
174
|
+
val count: Sink[Nothing, Any, Long]
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Count all elements in a stream:
|
|
179
|
+
|
|
180
|
+
```scala
|
|
181
|
+
import zio.blocks.streams._
|
|
182
|
+
|
|
183
|
+
val result = Stream(1, 2, 3, 4, 5).run(Sink.count)
|
|
184
|
+
// result: Either[Nothing, Long] = Right(5L)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### `Sink.sumInt` / `Sink.sumLong` / `Sink.sumFloat` / `Sink.sumDouble` — Typed Numeric Sums
|
|
188
|
+
|
|
189
|
+
Returns the sum of all elements as a numeric type. Each sink accepts the corresponding primitive type:
|
|
190
|
+
|
|
191
|
+
```scala
|
|
192
|
+
object Sink {
|
|
193
|
+
val sumInt: Sink[Nothing, Int, Long]
|
|
194
|
+
val sumLong: Sink[Nothing, Long, Long]
|
|
195
|
+
val sumFloat: Sink[Nothing, Float, Double]
|
|
196
|
+
val sumDouble: Sink[Nothing, Double, Double]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Note that `Sink.sumInt` returns `Long` (to avoid overflow) and `Sink.sumFloat` returns `Double` (to reduce rounding loss):
|
|
201
|
+
|
|
202
|
+
```scala
|
|
203
|
+
import zio.blocks.streams._
|
|
204
|
+
|
|
205
|
+
val intSum = Stream(1, 2, 3, 4, 5).run(Sink.sumInt)
|
|
206
|
+
// intSum: Either[Nothing, Long] = Right(15L)
|
|
207
|
+
|
|
208
|
+
val doubleSum = Stream(1.5, 2.5, 3.0).run(Sink.sumDouble)
|
|
209
|
+
// doubleSum: Either[Nothing, Double] = Right(7.0)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Construction
|
|
213
|
+
|
|
214
|
+
Sinks are created using factory methods on the companion object. These methods fall into several categories based on what they do:
|
|
215
|
+
|
|
216
|
+
### Collecting
|
|
217
|
+
|
|
218
|
+
Gather elements into collections:
|
|
219
|
+
|
|
220
|
+
#### `Sink.collectAll[A]` — Collect Into a Chunk
|
|
221
|
+
|
|
222
|
+
Collects all elements into a [`Chunk[A]`](../../chunk.md):
|
|
223
|
+
|
|
224
|
+
```scala
|
|
225
|
+
object Sink {
|
|
226
|
+
def collectAll[A]: Sink[Nothing, A, Chunk[A]]
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
This is the sink behind `Stream.runCollect`:
|
|
231
|
+
|
|
232
|
+
```scala
|
|
233
|
+
import zio.blocks.streams._
|
|
234
|
+
|
|
235
|
+
val result = Stream(1, 2, 3).run(Sink.collectAll[Int])
|
|
236
|
+
// result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 2, 3))
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### `Sink.take[A]` — Collect First N Elements
|
|
240
|
+
|
|
241
|
+
Collects at most `n` elements into a `Chunk[A]`, then stops (short-circuiting the upstream):
|
|
242
|
+
|
|
243
|
+
```scala
|
|
244
|
+
object Sink {
|
|
245
|
+
def take[A](n: Int): Sink[Nothing, A, Chunk[A]]
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Collect only the first three elements from a large stream:
|
|
250
|
+
|
|
251
|
+
```scala
|
|
252
|
+
import zio.blocks.streams._
|
|
253
|
+
|
|
254
|
+
val result = Stream.range(0, 1000).run(Sink.take(3))
|
|
255
|
+
// result: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(0, 1, 2))
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Aggregation and Search
|
|
259
|
+
|
|
260
|
+
These sinks combine elements into a single result or search for specific elements within a stream:
|
|
261
|
+
|
|
262
|
+
#### `Sink.foldLeft[A, Z]` — General Left Fold
|
|
263
|
+
|
|
264
|
+
Folds all elements using an accumulator function, starting from initial value `z`:
|
|
265
|
+
|
|
266
|
+
```scala
|
|
267
|
+
object Sink {
|
|
268
|
+
def foldLeft[A, Z](z: Z)(f: (Z, A) => Z)(implicit jtZ: JvmType.Infer[Z]): Sink[Nothing, A, Z]
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
This is the most general aggregation sink:
|
|
273
|
+
|
|
274
|
+
```scala
|
|
275
|
+
import zio.blocks.streams._
|
|
276
|
+
|
|
277
|
+
val sum = Stream(1, 2, 3, 4).run(Sink.foldLeft(0)(_ + _))
|
|
278
|
+
// sum: Either[Nothing, Int] = Right(10)
|
|
279
|
+
|
|
280
|
+
val concat = Stream("a", "b", "c").run(Sink.foldLeft("")(_ + _))
|
|
281
|
+
// concat: Either[Nothing, String] = Right("abc")
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### `Sink.head[A]` — First Element
|
|
285
|
+
|
|
286
|
+
Returns the first element wrapped in `Some`, or `None` for an empty stream:
|
|
287
|
+
|
|
288
|
+
```scala
|
|
289
|
+
object Sink {
|
|
290
|
+
def head[A]: Sink[Nothing, A, Option[A]]
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Get the first element from a stream, or None if empty:
|
|
295
|
+
|
|
296
|
+
```scala
|
|
297
|
+
import zio.blocks.streams._
|
|
298
|
+
|
|
299
|
+
val first = Stream(10, 20, 30).run(Sink.head[Int])
|
|
300
|
+
// first: Either[Nothing, Option[Int]] = Right(Some(10))
|
|
301
|
+
|
|
302
|
+
val empty = Stream.empty.run(Sink.head[Int])
|
|
303
|
+
// empty: Either[Nothing, Option[Int]] = Right(None)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### `Sink.last[A]` — Last Element
|
|
307
|
+
|
|
308
|
+
Returns the last element wrapped in `Some`, or `None` for an empty stream. Must consume all elements:
|
|
309
|
+
|
|
310
|
+
```scala
|
|
311
|
+
object Sink {
|
|
312
|
+
def last[A]: Sink[Nothing, A, Option[A]]
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Get the last element from a stream:
|
|
317
|
+
|
|
318
|
+
```scala
|
|
319
|
+
import zio.blocks.streams._
|
|
320
|
+
|
|
321
|
+
val result = Stream(10, 20, 30).run(Sink.last[Int])
|
|
322
|
+
// result: Either[Nothing, Option[Int]] = Right(Some(30))
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### `Sink.find[A]` — First Matching Element
|
|
326
|
+
|
|
327
|
+
Returns the first element satisfying `pred`, or `None`. Short-circuits on first match:
|
|
328
|
+
|
|
329
|
+
```scala
|
|
330
|
+
object Sink {
|
|
331
|
+
def find[A](pred: A => Boolean): Sink[Nothing, A, Option[A]]
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Find the first even number in the stream:
|
|
336
|
+
|
|
337
|
+
```scala
|
|
338
|
+
import zio.blocks.streams._
|
|
339
|
+
|
|
340
|
+
val found = Stream(1, 3, 4, 6).run(Sink.find[Int](_ % 2 == 0))
|
|
341
|
+
// found: Either[Nothing, Option[Int]] = Right(Some(4))
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### `Sink.exists[A]` — Any Element Matches
|
|
345
|
+
|
|
346
|
+
Returns `true` if any element satisfies `pred`. Short-circuits on first match:
|
|
347
|
+
|
|
348
|
+
```scala
|
|
349
|
+
object Sink {
|
|
350
|
+
def exists[A](pred: A => Boolean): Sink[Nothing, A, Boolean]
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Check if any element matches a condition:
|
|
355
|
+
|
|
356
|
+
```scala
|
|
357
|
+
import zio.blocks.streams._
|
|
358
|
+
|
|
359
|
+
val hasNegative = Stream(1, -2, 3).run(Sink.exists[Int](_ < 0))
|
|
360
|
+
// hasNegative: Either[Nothing, Boolean] = Right(true)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
#### `Sink.forall[A]` — All Elements Match
|
|
364
|
+
|
|
365
|
+
Returns `true` if all elements satisfy `pred`. Short-circuits to `false` on first failure:
|
|
366
|
+
|
|
367
|
+
```scala
|
|
368
|
+
object Sink {
|
|
369
|
+
def forall[A](pred: A => Boolean): Sink[Nothing, A, Boolean]
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Test whether all elements satisfy a condition:
|
|
374
|
+
|
|
375
|
+
```scala
|
|
376
|
+
import zio.blocks.streams._
|
|
377
|
+
|
|
378
|
+
val allPositive = Stream(1, 2, 3).run(Sink.forall[Int](_ > 0))
|
|
379
|
+
// allPositive: Either[Nothing, Boolean] = Right(true)
|
|
380
|
+
|
|
381
|
+
val notAll = Stream(1, -2, 3).run(Sink.forall[Int](_ > 0))
|
|
382
|
+
// notAll: Either[Nothing, Boolean] = Right(false)
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Effectful
|
|
386
|
+
|
|
387
|
+
These sinks perform side effects during stream consumption:
|
|
388
|
+
|
|
389
|
+
#### `Sink.foreach[A]` — Apply Side Effect to Each Element
|
|
390
|
+
|
|
391
|
+
Applies `f` to every element for side effects. Returns `Unit`:
|
|
392
|
+
|
|
393
|
+
```scala
|
|
394
|
+
object Sink {
|
|
395
|
+
def foreach[A](f: A => Unit): Sink[Nothing, A, Unit]
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Print each element as it is processed:
|
|
400
|
+
|
|
401
|
+
```scala
|
|
402
|
+
import zio.blocks.streams._
|
|
403
|
+
|
|
404
|
+
val result = Stream(1, 2, 3).run(Sink.foreach[Int](x => println(s"Got: $x")))
|
|
405
|
+
// Got: 1
|
|
406
|
+
// Got: 2
|
|
407
|
+
// Got: 3
|
|
408
|
+
// result: Either[Nothing, Unit] = Right(())
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Async Aggregation and Search
|
|
412
|
+
|
|
413
|
+
Five of the aggregation, search, and effectful sinks have an `*Async` twin whose callback returns `Async` instead of a value. The twin consumes the same input and returns the same result type; only the callback's shape differs:
|
|
414
|
+
|
|
415
|
+
```scala
|
|
416
|
+
object Sink {
|
|
417
|
+
def existsAsync[A](pred: A => Async[Boolean]): Sink[Nothing, A, Boolean]
|
|
418
|
+
def findAsync[A](pred: A => Async[Boolean]): Sink[Nothing, A, Option[A]]
|
|
419
|
+
def foldLeftAsync[A, Z](z: Z)(f: (Z, A) => Async[Z])(implicit jtZ: JvmType.Infer[Z]): Sink[Nothing, A, Z]
|
|
420
|
+
def forallAsync[A](pred: A => Async[Boolean]): Sink[Nothing, A, Boolean]
|
|
421
|
+
def foreachAsync[A](f: A => Async[Unit]): Sink[Nothing, A, Unit]
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Each stops where its synchronous twin stops:
|
|
426
|
+
|
|
427
|
+
| Async sink | Sync twin | Result | Stops pulling at |
|
|
428
|
+
|----------------------|-----------------|-------------|--------------------------------------------------------------|
|
|
429
|
+
| `Sink.existsAsync` | `Sink.exists` | `Boolean` | the first `true`; otherwise end-of-stream, returning `false` |
|
|
430
|
+
| `Sink.findAsync` | `Sink.find` | `Option[A]` | the first match; otherwise end-of-stream, returning `None` |
|
|
431
|
+
| `Sink.foldLeftAsync` | `Sink.foldLeft` | `Z` | end-of-stream |
|
|
432
|
+
| `Sink.forallAsync` | `Sink.forall` | `Boolean` | the first `false`; otherwise end-of-stream, returning `true` |
|
|
433
|
+
| `Sink.foreachAsync` | `Sink.foreach` | `Unit` | end-of-stream |
|
|
434
|
+
|
|
435
|
+
Three properties hold across all five. At most one callback effect runs at a time, in encounter order, so these are sequential and back-pressured rather than concurrent — for concurrency, put it in the stream with [`Stream#mapParAsync`](./stream.md#streammapparasync) before the sink. No effect is started for input the sink never pulls, so short-circuiting still costs nothing after the decisive element. And a callback that fails or is cancelled fails or cancels the run as a *defect*, not as a value in the sink's typed error channel `E` — all five have `E = Nothing`.
|
|
436
|
+
|
|
437
|
+
`Sink.foldLeftAsync` carries the same `JvmType.Infer[Z]` evidence as `Sink.foldLeft`. It keeps the asynchronous fold aligned with the specialized synchronous one; the semantics are identical.
|
|
438
|
+
|
|
439
|
+
```scala
|
|
440
|
+
import zio.blocks.async._
|
|
441
|
+
import zio.blocks.streams._
|
|
442
|
+
|
|
443
|
+
val readings = Stream(12, 7, 30, 21, 5)
|
|
444
|
+
|
|
445
|
+
// Sequential: one predicate effect at a time, stopping at 30.
|
|
446
|
+
val tooHot: Async[Either[Nothing, Boolean]] =
|
|
447
|
+
readings.runAsync(Sink.existsAsync[Int](c => Async.succeed(c > 25)))
|
|
448
|
+
|
|
449
|
+
// The fold visits every element; the accumulator is threaded through Async.
|
|
450
|
+
val total: Async[Either[Nothing, Long]] =
|
|
451
|
+
readings.runAsync(Sink.foldLeftAsync(0L)((sum: Long, c: Int) => Async.succeed(sum + c)))
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Failing
|
|
455
|
+
|
|
456
|
+
These sinks can be used to produce typed errors or fail under specific conditions:
|
|
457
|
+
|
|
458
|
+
#### `Sink.fail[E]` — Immediately Fail
|
|
459
|
+
|
|
460
|
+
Creates a sink that fails immediately with a typed error, without consuming any elements:
|
|
461
|
+
|
|
462
|
+
```scala
|
|
463
|
+
object Sink {
|
|
464
|
+
def fail[E](e: E): Sink[E, Any, Nothing]
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Use this in conditional sink construction:
|
|
469
|
+
|
|
470
|
+
```scala
|
|
471
|
+
import zio.blocks.streams._
|
|
472
|
+
|
|
473
|
+
val sink: Sink[String, Int, Long] =
|
|
474
|
+
if (false) Sink.count
|
|
475
|
+
else Sink.fail("not ready")
|
|
476
|
+
// sink: Sink[String, Int, Long] = zio.blocks.streams.Sink$$anon$7@7fdf129
|
|
477
|
+
|
|
478
|
+
val result = Stream(1, 2, 3).run(sink)
|
|
479
|
+
// result: Either[String, Long] = Left("not ready")
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### I/O
|
|
483
|
+
|
|
484
|
+
Write elements to Java I/O destinations:
|
|
485
|
+
|
|
486
|
+
#### `Sink.fromOutputStream` — Write Bytes
|
|
487
|
+
|
|
488
|
+
Writes every `Byte` element to a `java.io.OutputStream`:
|
|
489
|
+
|
|
490
|
+
```scala
|
|
491
|
+
object Sink {
|
|
492
|
+
def fromOutputStream(os: java.io.OutputStream): Sink[Nothing, Byte, Unit]
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
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:
|
|
496
|
+
|
|
497
|
+
```scala
|
|
498
|
+
import zio.blocks.streams._
|
|
499
|
+
import java.io.ByteArrayOutputStream
|
|
500
|
+
|
|
501
|
+
val bos = new ByteArrayOutputStream()
|
|
502
|
+
// bos: ByteArrayOutputStream = Hi!
|
|
503
|
+
|
|
504
|
+
// Write first batch of bytes
|
|
505
|
+
Stream.fromChunk(zio.blocks.chunk.Chunk[Byte](72, 105)).run(Sink.fromOutputStream(bos))
|
|
506
|
+
// res15: Either[Nothing, Unit] = Right(())
|
|
507
|
+
|
|
508
|
+
// Write second batch to the same stream (reuse it)
|
|
509
|
+
Stream.fromChunk(zio.blocks.chunk.Chunk[Byte](33)).run(Sink.fromOutputStream(bos))
|
|
510
|
+
// res16: Either[Nothing, Unit] = Right(())
|
|
511
|
+
|
|
512
|
+
// When done writing all batches, YOU close the stream
|
|
513
|
+
bos.close()
|
|
514
|
+
|
|
515
|
+
// ByteArrayOutputStream ignores close(), so you can still call toByteArray()
|
|
516
|
+
val allBytes = bos.toByteArray()
|
|
517
|
+
// allBytes: Array[Byte] = Array(72, 105, 33)
|
|
518
|
+
// This works because ByteArrayOutputStream doesn't maintain any closeable resources
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
#### `Sink.fromJavaWriter` — Write Characters
|
|
522
|
+
|
|
523
|
+
Writes every `Char` element to a `java.io.Writer`. Does not close the writer when done — you own its lifecycle:
|
|
524
|
+
|
|
525
|
+
```scala
|
|
526
|
+
object Sink {
|
|
527
|
+
def fromJavaWriter(w: java.io.Writer): Sink[Nothing, Char, Unit]
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Write a stream of characters to a StringWriter and access the accumulated text:
|
|
532
|
+
|
|
533
|
+
```scala
|
|
534
|
+
import zio.blocks.streams._
|
|
535
|
+
import java.io.StringWriter
|
|
536
|
+
|
|
537
|
+
val writer = new StringWriter()
|
|
538
|
+
// writer: StringWriter = Hello World
|
|
539
|
+
|
|
540
|
+
// Write a stream of individual characters
|
|
541
|
+
Stream('H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd')
|
|
542
|
+
.run(Sink.fromJavaWriter(writer))
|
|
543
|
+
// res19: Either[Nothing, Unit] = Right(())
|
|
544
|
+
|
|
545
|
+
// Get the final string
|
|
546
|
+
val result = writer.toString()
|
|
547
|
+
// result: String = "Hello World"
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
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.
|
|
551
|
+
|
|
552
|
+
### Custom
|
|
553
|
+
|
|
554
|
+
Advanced low-level use cases with direct reader protocol access:
|
|
555
|
+
|
|
556
|
+
#### Custom Sink Factories
|
|
557
|
+
|
|
558
|
+
`createAsync` is the cross-platform escape hatch for a native asynchronous drain. `createBoth` supplies independent native synchronous and asynchronous drains. The plain `create` factory is JVM-only because its callback consumes a blocking `SyncReader`:
|
|
559
|
+
|
|
560
|
+
```scala
|
|
561
|
+
object Sink {
|
|
562
|
+
def createAsync[E, A, Z](f: Reader.AsyncReader[A] => Async[Z]): Sink[E, A, Z]
|
|
563
|
+
def createBoth[E, A, Z](sync: Reader.SyncReader[A] => Z,
|
|
564
|
+
async: Reader.AsyncReader[A] => Async[Z]): Sink[E, A, Z]
|
|
565
|
+
// JVM only
|
|
566
|
+
def create[E, A, Z](f: Reader.SyncReader[A] => Z): Sink[E, A, Z]
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
:::note
|
|
571
|
+
These factories give you direct access to the reader protocol. Await asynchronous pulls sequentially and prefer built-in sinks when possible.
|
|
572
|
+
:::
|
|
573
|
+
|
|
574
|
+
Here's a custom sink that computes the average of all integers in a stream:
|
|
575
|
+
|
|
576
|
+
```scala
|
|
577
|
+
import zio.blocks.streams._
|
|
578
|
+
import zio.blocks.streams.io.Reader
|
|
579
|
+
|
|
580
|
+
// A custom sink that computes the average of Ints
|
|
581
|
+
val average = Sink.create[Nothing, Int, Double] { reader =>
|
|
582
|
+
def loop(sum: Long, count: Long): (Long, Long) = {
|
|
583
|
+
val v = reader.readInt(Long.MinValue)
|
|
584
|
+
if (v == Long.MinValue) (sum, count)
|
|
585
|
+
else {
|
|
586
|
+
val newSum = sum + v
|
|
587
|
+
loop(newSum, count + 1)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
val (sum, count) = loop(0L, 0L)
|
|
591
|
+
if (count == 0) 0.0 else sum.toDouble / count
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
This example shows how `Sink.create` works. `readInt` widens an `Int` to `Long`, leaving `Long.MinValue` available as an out-of-domain end marker. For full-domain `Long` and `Double` inputs, do not choose a data sentinel: allocate a reusable one-element primitive array and use `readLongs` or `readDoubles`, whose returned count reports data or end-of-stream without collisions. You'd use `Sink.create` when no built-in sink provides the exact aggregation or transformation logic you need — it is powerful but requires understanding the low-level [Reader protocol](../primitives/reader.md).
|
|
596
|
+
|
|
597
|
+
## Transforming Sinks
|
|
598
|
+
|
|
599
|
+
Every sink can be transformed using these instance methods:
|
|
600
|
+
|
|
601
|
+
### `Sink#contramap[A2]` — Pre-Process Input
|
|
602
|
+
|
|
603
|
+
Transforms the input elements before they reach the sink. The sink's result and error types are unchanged:
|
|
604
|
+
|
|
605
|
+
```scala
|
|
606
|
+
abstract class Sink[+E, -A, +Z] {
|
|
607
|
+
def contramap[A0 <: A, A2](g: A2 => A0)(implicit jtA0: JvmType.Infer[A0]): Sink[E, A2, Z]
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
The evidence describes the callback's **result** type `A0`, not the new sink input `A2`. The bound `A0 <: A` is variance-sound for contravariant `A`, while invariant `JvmType.Infer[A0]` lets the mapped reader advertise the callback's exact physical result lane. Primitive results therefore retain their exact lane; the `AnyRef` fallback deliberately uses the boxed reference lane. `contramapAsync` has the same evidence and representation rules for `A2 => Async[A0]`. Both adapters transform only elements requested by the wrapped sink, preserve short-circuiting, and leave reader closure to the run terminal.
|
|
612
|
+
|
|
613
|
+
`Sink#contramap` is the dual of `Sink#map`: it transforms what goes *in*, not what comes *out*:
|
|
614
|
+
|
|
615
|
+
```scala
|
|
616
|
+
import zio.blocks.streams._
|
|
617
|
+
|
|
618
|
+
// A sink that counts the length of strings
|
|
619
|
+
val totalLength: Sink[Nothing, String, Long] =
|
|
620
|
+
Sink.sumInt.contramap[Int, String](_.length)
|
|
621
|
+
// totalLength: Sink[Nothing, String, Long] = zio.blocks.streams.Sink$Contramapped@288bf077
|
|
622
|
+
|
|
623
|
+
val result = Stream("hello", "world").run(totalLength)
|
|
624
|
+
// result: Either[Nothing, Long] = Right(10L)
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### `Sink#map[Z2]` — Transform Result
|
|
628
|
+
|
|
629
|
+
Transforms the result after the sink finishes draining:
|
|
630
|
+
|
|
631
|
+
```scala
|
|
632
|
+
abstract class Sink[+E, -A, +Z] {
|
|
633
|
+
def map[Z2](f: Z => Z2): Sink[E, A, Z2]
|
|
634
|
+
}
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Transform the result after draining:
|
|
638
|
+
|
|
639
|
+
```scala
|
|
640
|
+
import zio.blocks.streams._
|
|
641
|
+
|
|
642
|
+
val countAsString: Sink[Nothing, Any, String] =
|
|
643
|
+
Sink.count.map(n => s"Total: $n elements")
|
|
644
|
+
// countAsString: Sink[Nothing, Any, String] = zio.blocks.streams.Sink$Mapped@36040911
|
|
645
|
+
|
|
646
|
+
val result = Stream(1, 2, 3).run(countAsString)
|
|
647
|
+
// result: Either[Nothing, String] = Right("Total: 3 elements")
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### `Sink#mapError[E2]` — Transform Error
|
|
651
|
+
|
|
652
|
+
Transforms the error channel of a sink:
|
|
653
|
+
|
|
654
|
+
```scala
|
|
655
|
+
abstract class Sink[+E, -A, +Z] {
|
|
656
|
+
def mapError[E2](f: E => E2)(implicit isNothing: Sink.IsNothing[E]): Sink[E2, A, Z]
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
The `IsNothing` evidence records whether `E` is `Nothing`. For an infallible sink the method returns the same sink without allocating a wrapper; otherwise it maps typed errors:
|
|
661
|
+
|
|
662
|
+
```scala
|
|
663
|
+
import zio.blocks.streams._
|
|
664
|
+
|
|
665
|
+
// No-op: drain never fails, so mapError is free
|
|
666
|
+
val mapped = Sink.drain.mapError[String](_.toString)
|
|
667
|
+
// At compile time: this is just a cast, no wrapper allocated
|
|
668
|
+
|
|
669
|
+
// Real mapping: fail can produce errors
|
|
670
|
+
val failing = Sink.fail("oops").mapError[RuntimeException](new RuntimeException(_))
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
### `Sink#contramapAsync[A2]` — Pre-Process Input Asynchronously
|
|
674
|
+
|
|
675
|
+
Applies an asynchronous function to each element before it reaches the sink:
|
|
676
|
+
|
|
677
|
+
```scala
|
|
678
|
+
abstract class Sink[+E, -A, +Z] {
|
|
679
|
+
def contramapAsync[A0 <: A, A2](g: A2 => Async[A0])(implicit jtA0: JvmType.Infer[A0]): Sink[E, A2, Z]
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
The evidence is the same as `Sink#contramap`'s and targets the callback's **result** type `A0`, not the new sink input `A2`. `g` runs sequentially, one effect at a time, as the wrapped sink requests elements, so short-circuiting is unchanged and `g` is never started for input the sink does not pull. A failed or cancelled `g` fails or cancels the run as a defect and stops pulling input; it does not appear in the sink's typed error channel.
|
|
684
|
+
|
|
685
|
+
```scala
|
|
686
|
+
import zio.blocks.async._
|
|
687
|
+
import zio.blocks.streams._
|
|
688
|
+
|
|
689
|
+
// Resolve each identifier to a length before counting it
|
|
690
|
+
val totalLength: Sink[Nothing, String, Long] =
|
|
691
|
+
Sink.sumInt.contramapAsync[Int, String](id => Async.succeed(id.length))
|
|
692
|
+
|
|
693
|
+
val result: Async[Either[Nothing, Long]] =
|
|
694
|
+
Stream("hello", "world").runAsync(totalLength)
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### `Sink#mapAsync[Z2]` — Transform Result Asynchronously
|
|
698
|
+
|
|
699
|
+
Transforms the result asynchronously after the sink finishes draining:
|
|
700
|
+
|
|
701
|
+
```scala
|
|
702
|
+
abstract class Sink[+E, -A, +Z] {
|
|
703
|
+
def mapAsync[Z2](f: Z => Async[Z2]): Sink[E, A, Z2]
|
|
704
|
+
}
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
`f` runs once, after the drain completes, so it does not change input consumption. When the sink fails, `f` is never started. Failure or cancellation of `f` fails or cancels the run as a defect.
|
|
708
|
+
|
|
709
|
+
```scala
|
|
710
|
+
import zio.blocks.async._
|
|
711
|
+
import zio.blocks.streams._
|
|
712
|
+
|
|
713
|
+
val persistedCount: Sink[Nothing, Any, String] =
|
|
714
|
+
Sink.count.mapAsync(n => Async.succeed(s"Total: $n elements"))
|
|
715
|
+
|
|
716
|
+
val result: Async[Either[Nothing, String]] =
|
|
717
|
+
Stream(1, 2, 3).runAsync(persistedCount)
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
### `Sink#mapErrorAsync[E2]` — Transform Error Asynchronously
|
|
721
|
+
|
|
722
|
+
Transforms the typed error channel asynchronously:
|
|
723
|
+
|
|
724
|
+
```scala
|
|
725
|
+
abstract class Sink[+E, -A, +Z] {
|
|
726
|
+
def mapErrorAsync[E2](f: E => Async[E2])(implicit isNothing: Sink.IsNothing[E]): Sink[E2, A, Z]
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
It carries the same `IsNothing` evidence as `Sink#mapError`: for an infallible sink (`E = Nothing`) the method returns the same sink without allocating a wrapper, and `f` can never be evaluated. Successful results, upstream stream errors, and defects pass through untransformed; only the sink's own typed error reaches `f`. Failure or cancellation of `f` is a defect.
|
|
731
|
+
|
|
732
|
+
```scala
|
|
733
|
+
import zio.blocks.async._
|
|
734
|
+
import zio.blocks.streams._
|
|
735
|
+
|
|
736
|
+
// Infallible: no wrapper is allocated and `f` can never run
|
|
737
|
+
val mapped = Sink.drain.mapErrorAsync[String](e => Async.succeed(e.toString))
|
|
738
|
+
|
|
739
|
+
// Fallible: the typed error is transformed before it reaches the `Left`
|
|
740
|
+
val failing = Sink.fail("oops").mapErrorAsync[RuntimeException](e => Async.succeed(new RuntimeException(e)))
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
## Integration with Stream
|
|
744
|
+
|
|
745
|
+
`Stream.run(sink)` is the primary entry point. ZIO Blocks also provides convenience methods on `Stream` that delegate to built-in sinks:
|
|
746
|
+
|
|
747
|
+
| Stream method | Equivalent Sink |
|
|
748
|
+
|------------------------|-----------------------------------|
|
|
749
|
+
| `stream.runCollect` | `stream.run(Sink.collectAll)` |
|
|
750
|
+
| `stream.runDrain` | `stream.run(Sink.drain)` |
|
|
751
|
+
| `stream.runForeach(f)` | `stream.run(Sink.foreach(f))` |
|
|
752
|
+
| `stream.runFold(z)(f)` | `stream.run(Sink.foldLeft(z)(f))` |
|
|
753
|
+
| `stream.count` | `stream.run(Sink.count)` |
|
|
754
|
+
| `stream.head` | `stream.run(Sink.head)` |
|
|
755
|
+
| `stream.last` | `stream.run(Sink.last)` |
|
|
756
|
+
| `stream.find(pred)` | `stream.run(Sink.find(pred))` |
|
|
757
|
+
| `stream.exists(pred)` | `stream.run(Sink.exists(pred))` |
|
|
758
|
+
| `stream.forall(pred)` | `stream.run(Sink.forall(pred))` |
|
|
759
|
+
|
|
760
|
+
The `runFold` method with primitive accumulator types (`Int`, `Long`, `Double`) uses specialized internal sink classes that keep the accumulator unboxed.
|
|
761
|
+
|
|
762
|
+
See [Stream — Running Streams](./stream.md#running-streams) for more details on terminal operations.
|
|
763
|
+
|
|
764
|
+
## Integration with Pipeline
|
|
765
|
+
|
|
766
|
+
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:
|
|
767
|
+
|
|
768
|
+
```scala
|
|
769
|
+
import zio.blocks.streams._
|
|
770
|
+
import zio.blocks.chunk.Chunk
|
|
771
|
+
|
|
772
|
+
val cleanAndCollect: Sink[Nothing, String, Chunk[String]] =
|
|
773
|
+
Pipeline.map[String, String](_.trim.toLowerCase)
|
|
774
|
+
.andThenSink(Sink.collectAll[String])
|
|
775
|
+
// cleanAndCollect: Sink[Nothing, String, Chunk[String]] = zio.blocks.streams.Sink$Contramapped@5d54c379
|
|
776
|
+
|
|
777
|
+
val result = Stream(" Hello ", " WORLD ").run(cleanAndCollect)
|
|
778
|
+
// result: Either[Nothing, Chunk[String]] = Right(IndexedSeq("hello", "world"))
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
The equivalence law holds: `stream.via(pipe).run(sink) == stream.run(pipe.andThenSink(sink))`.
|
|
782
|
+
|
|
783
|
+
See [Pipeline — Applying to a Sink](./pipeline.md#applying-to-a-sink) for more details.
|
|
784
|
+
|
|
785
|
+
## JVM NIO Sinks
|
|
786
|
+
|
|
787
|
+
The `NioSinks` object (JVM-only) provides sinks for Java NIO (`java.nio`) buffers and channels. NIO offers efficient buffers and both blocking and selector-based channel APIs, but these sinks use blocking channel writes; they do not expose selector-based non-blocking output. When you're writing to network sockets, files, or other NIO-based resources, these sinks give you a convenient way to drain streams directly into NIO data structures.
|
|
788
|
+
|
|
789
|
+
Traditional Java I/O (`OutputStream`, `Writer`) blocks threads and requires manual buffering for efficiency. `NioSinks.fromChannel` also blocks, but handles buffer allocation, position management, and flushing automatically (default 8KB), while typed variants like `NioSinks.fromByteBufferInt` and `NioSinks.fromByteBufferLong` eliminate boxing overhead by writing primitives directly to buffers you provide.
|
|
790
|
+
|
|
791
|
+
Choose `NioSinks.fromChannel` when blocking channel output is acceptable and you want automatic buffering for network sockets or files. Choose typed variants when you control buffer allocation and are streaming millions of primitives where boxing would degrade performance.
|
|
792
|
+
|
|
793
|
+
Each of these sinks implements both drains, so NIO output works under the blocking `Stream#run` and under `Stream#runAsync` alike. What the asynchronous drain removes is the thread parked waiting for stream *input*; the destination writes are the same blocking `java.nio` calls on either path. `NioSinks.fromChannel` does budget its asynchronous flush loop: when a channel write makes no progress it yields instead of spinning, so a stalled channel does not monopolise the caller. [Reader](../primitives/reader.md#from-native-asynchronous-sources) covers the NIO adapters on the source side in full, and this page does not duplicate them.
|
|
794
|
+
|
|
795
|
+
Here are the available NIO sinks:
|
|
796
|
+
|
|
797
|
+
```scala
|
|
798
|
+
object NioSinks {
|
|
799
|
+
def fromByteBuffer (buf: ByteBuffer): Sink[Nothing, Byte, Unit]
|
|
800
|
+
def fromByteBufferInt (buf: ByteBuffer): Sink[Nothing, Int, Unit]
|
|
801
|
+
def fromByteBufferLong (buf: ByteBuffer): Sink[Nothing, Long, Unit]
|
|
802
|
+
def fromByteBufferFloat (buf: ByteBuffer): Sink[Nothing, Float, Unit]
|
|
803
|
+
def fromByteBufferDouble(buf: ByteBuffer): Sink[Nothing, Double, Unit]
|
|
804
|
+
def fromChannel(ch: WritableByteChannel, bufSize: Int = 8192): Sink[IOException, Byte, Unit]
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### From ByteBuffer Sinks
|
|
809
|
+
|
|
810
|
+
**`NioSinks.fromByteBuffer` and typed variants** — Write primitive streams directly into a pre-allocated NIO ByteBuffer:
|
|
811
|
+
- `NioSinks.fromByteBuffer` — writes individual `Byte` elements.
|
|
812
|
+
- `NioSinks.fromByteBufferInt`, `NioSinks.fromByteBufferLong`, `NioSinks.fromByteBufferFloat`, `NioSinks.fromByteBufferDouble` — write primitives directly using the buffer's native methods (`putInt`, `putLong`, etc.), one buffer write per element rather than one per byte.
|
|
813
|
+
|
|
814
|
+
Here's an example using ByteBuffer with typed primitive writes:
|
|
815
|
+
|
|
816
|
+
```scala
|
|
817
|
+
import zio.blocks.streams._
|
|
818
|
+
import zio.blocks.streams.NioSinks
|
|
819
|
+
import java.nio.ByteBuffer
|
|
820
|
+
import java.nio.ByteOrder
|
|
821
|
+
|
|
822
|
+
val buffer = ByteBuffer.allocate(32).order(ByteOrder.BIG_ENDIAN)
|
|
823
|
+
// buffer: ByteBuffer = java.nio.HeapByteBuffer[pos=32 lim=32 cap=32]
|
|
824
|
+
|
|
825
|
+
// Write a stream of Longs to the buffer
|
|
826
|
+
Stream(1L, 2L, 3L, 4L).run(NioSinks.fromByteBufferLong(buffer))
|
|
827
|
+
// res29: Either[Nothing, Unit] = Right(())
|
|
828
|
+
|
|
829
|
+
// After writing, rewind to read
|
|
830
|
+
buffer.rewind()
|
|
831
|
+
// res30: ByteBuffer = java.nio.HeapByteBuffer[pos=32 lim=32 cap=32]
|
|
832
|
+
|
|
833
|
+
val readBack = List(
|
|
834
|
+
buffer.getLong(),
|
|
835
|
+
buffer.getLong(),
|
|
836
|
+
buffer.getLong(),
|
|
837
|
+
buffer.getLong()
|
|
838
|
+
)
|
|
839
|
+
// readBack: List[Long] = List(1L, 2L, 3L, 4L)
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
This example allocates a 32-byte buffer (4 Longs × 8 bytes each), writes four `Long` values using `NioSinks.fromByteBufferLong` (which calls `putLong` on each element), then rewinds and reads them back to verify. The typed variant writes one 8-byte buffer operation per element, where the byte variant would require the stream to carry each byte separately.
|
|
843
|
+
|
|
844
|
+
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 let each element be written through its exact primitive lane without boxing.
|
|
845
|
+
|
|
846
|
+
Here is the complete example:
|
|
847
|
+
|
|
848
|
+
```scala title="streams-examples/src/main/scala/sink/SinkScientificComputingExample.scala"
|
|
849
|
+
package sink
|
|
850
|
+
|
|
851
|
+
import zio.blocks.streams.*
|
|
852
|
+
import zio.blocks.streams.NioSinks
|
|
853
|
+
import java.nio.ByteBuffer
|
|
854
|
+
import scala.math.pow
|
|
855
|
+
|
|
856
|
+
object SinkScientificComputingExample extends App {
|
|
857
|
+
println("=== Batching Doubles for Scientific Computing ===\n")
|
|
858
|
+
|
|
859
|
+
// Simulate raw sensor measurements that need calibration
|
|
860
|
+
println("Scenario: Streaming voltage sensor measurements with calibration curve\n")
|
|
861
|
+
|
|
862
|
+
val measurementCount = 10
|
|
863
|
+
val bufferCapacity = measurementCount * 8 // 8 bytes per Double
|
|
864
|
+
|
|
865
|
+
println(s"Processing $measurementCount measurements...\n")
|
|
866
|
+
|
|
867
|
+
// Generate raw voltage readings (0.0 to 0.09)
|
|
868
|
+
val rawVoltages = (0 until measurementCount).map(i => (i * 0.01).toDouble).toList
|
|
869
|
+
|
|
870
|
+
println("Raw measurements (voltage):")
|
|
871
|
+
rawVoltages.zipWithIndex.foreach { case (v, i) =>
|
|
872
|
+
println(f" [$i] $v%.4f V")
|
|
873
|
+
}
|
|
874
|
+
println()
|
|
875
|
+
|
|
876
|
+
// Process and calibrate measurements
|
|
877
|
+
val buffer = processAndBufferMeasurements(measurementCount, bufferCapacity)
|
|
878
|
+
|
|
879
|
+
println("After calibration (applied quadratic correction: V' = V × (1 + 0.05×V²)):\n")
|
|
880
|
+
|
|
881
|
+
// Read back and display calibrated values
|
|
882
|
+
buffer.rewind()
|
|
883
|
+
var index = 0
|
|
884
|
+
while (buffer.hasRemaining) {
|
|
885
|
+
val calibrated = buffer.getDouble()
|
|
886
|
+
println(f" [$index] $calibrated%.6f V")
|
|
887
|
+
index += 1
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
println("\n=== Pattern Use Cases ===")
|
|
891
|
+
println("This pattern is used in:")
|
|
892
|
+
println(" • Scientific instrumentation (analog-to-digital conversion)")
|
|
893
|
+
println(" • Machine learning pipelines (sensor data → training batches)")
|
|
894
|
+
println(" • Signal processing (raw signals → preprocessed data → computation)")
|
|
895
|
+
println("\nKey benefits:")
|
|
896
|
+
println(" • Zero-copy batch processing with DirectByteBuffer")
|
|
897
|
+
println(" • Efficient numerical stream transformation")
|
|
898
|
+
println(" • Memory-friendly for large datasets")
|
|
899
|
+
|
|
900
|
+
// Process and buffer measurements using NioSinks
|
|
901
|
+
def processAndBufferMeasurements(
|
|
902
|
+
measurementCount: Int,
|
|
903
|
+
bufferCapacity: Int
|
|
904
|
+
): ByteBuffer = {
|
|
905
|
+
val buffer = ByteBuffer.allocateDirect(bufferCapacity)
|
|
906
|
+
|
|
907
|
+
// Stream of raw voltage measurements (need calibration)
|
|
908
|
+
val voltages = Stream.range(0, measurementCount).map(i => (i * 0.01).toDouble)
|
|
909
|
+
|
|
910
|
+
// Apply calibration curve: quadratic correction
|
|
911
|
+
// This simulates real sensor calibration with nonlinear response
|
|
912
|
+
val calibrated = voltages.map { raw =>
|
|
913
|
+
val calibrationFactor = 1.0 + (0.05 * pow(raw, 2))
|
|
914
|
+
raw * calibrationFactor
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// Write calibrated values directly to ByteBuffer using typed sink
|
|
918
|
+
// This is much faster than element-by-element byte writing
|
|
919
|
+
calibrated.run(NioSinks.fromByteBufferDouble(buffer))
|
|
920
|
+
buffer.flip()
|
|
921
|
+
buffer
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
```
|
|
925
|
+
|
|
926
|
+
Run this example with:
|
|
927
|
+
|
|
928
|
+
```bash
|
|
929
|
+
sbt "streams-examples/runMain sink.SinkScientificComputingExample"
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
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.
|
|
933
|
+
|
|
934
|
+
The typed sinks dispatch through their exact primitive lanes. `Long` and `Double` use collision-free bulk-count status, so `Long.MinValue`, `Long.MaxValue`, every finite or infinite `Double`, signed zero, and every raw NaN payload are written as ordinary data. There is no sentinel-value truncation restriction. The supplied buffer remains caller-owned and is not flipped, rewound, or closed by the sink. [Zero-Boxing Streams](../execution-and-compatibility/zero-boxing.md) carries the per-lane end-of-stream table this rests on.
|
|
935
|
+
|
|
936
|
+
### From Channel Sink
|
|
937
|
+
|
|
938
|
+
The **`NioSinks.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. It does not close the caller-owned channel.
|
|
939
|
+
|
|
940
|
+
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.
|
|
941
|
+
|
|
942
|
+
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.
|
|
943
|
+
|
|
944
|
+
Here is the complete example:
|
|
945
|
+
|
|
946
|
+
```scala title="streams-examples/src/main/scala/sink/SinkTelemetryExample.scala"
|
|
947
|
+
package sink
|
|
948
|
+
|
|
949
|
+
import zio.blocks.streams.*
|
|
950
|
+
import zio.blocks.streams.NioSinks
|
|
951
|
+
import java.io.RandomAccessFile
|
|
952
|
+
import java.nio.file.Files
|
|
953
|
+
import scala.util.Using
|
|
954
|
+
|
|
955
|
+
object SinkTelemetryExample extends App {
|
|
956
|
+
println("=== Streaming Telemetry to File Channel ===\n")
|
|
957
|
+
|
|
958
|
+
// Simulated sensor readings (timestamp, temperature)
|
|
959
|
+
case class SensorReading(timestamp: Long, temperature: Double) {
|
|
960
|
+
override def toString: String = f"[$timestamp] $temperature%.2f°C"
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Generate mock sensor data
|
|
964
|
+
val sensorReadings = List(
|
|
965
|
+
SensorReading(1000L, 22.5),
|
|
966
|
+
SensorReading(1100L, 23.1),
|
|
967
|
+
SensorReading(1200L, 22.8),
|
|
968
|
+
SensorReading(1300L, 23.4),
|
|
969
|
+
SensorReading(1400L, 24.0)
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
println("Sensor readings to write:")
|
|
973
|
+
sensorReadings.foreach(r => println(s" $r"))
|
|
974
|
+
println()
|
|
975
|
+
|
|
976
|
+
// Write telemetry to file using NioSinks.fromChannel
|
|
977
|
+
val tempFile = Files.createTempFile("telemetry", ".bin")
|
|
978
|
+
val filePath = tempFile.toString
|
|
979
|
+
|
|
980
|
+
println(s"Writing to $filePath...")
|
|
981
|
+
Using(new RandomAccessFile(filePath, "rw")) { file =>
|
|
982
|
+
val channel = file.getChannel
|
|
983
|
+
channel.truncate(0) // Clear file
|
|
984
|
+
|
|
985
|
+
// Serialize readings into a single buffer: 8 bytes timestamp + 8 bytes temperature per reading
|
|
986
|
+
val buffer = java.nio.ByteBuffer.allocate(sensorReadings.length * 16)
|
|
987
|
+
sensorReadings.foreach { reading =>
|
|
988
|
+
buffer.putLong(reading.timestamp)
|
|
989
|
+
buffer.putDouble(reading.temperature)
|
|
990
|
+
}
|
|
991
|
+
buffer.flip()
|
|
992
|
+
|
|
993
|
+
// Write all bytes to file with internal buffering (8KB chunks)
|
|
994
|
+
val bytes = buffer.array()
|
|
995
|
+
val byteStream = Stream.fromChunk(zio.blocks.chunk.Chunk.fromArray(bytes))
|
|
996
|
+
byteStream.run(NioSinks.fromChannel(channel, bufSize = 8192))
|
|
997
|
+
|
|
998
|
+
println(s"✓ Wrote ${file.length()} bytes to disk")
|
|
999
|
+
}.get
|
|
1000
|
+
|
|
1001
|
+
// Read back and verify
|
|
1002
|
+
println("\nVerifying written data:")
|
|
1003
|
+
Using(new RandomAccessFile(filePath, "r")) { file =>
|
|
1004
|
+
val buf = java.nio.ByteBuffer.allocate((8 + 8) * sensorReadings.length)
|
|
1005
|
+
file.getChannel.read(buf)
|
|
1006
|
+
buf.rewind()
|
|
1007
|
+
|
|
1008
|
+
var count = 0
|
|
1009
|
+
while (buf.remaining() >= 16) {
|
|
1010
|
+
val timestamp = buf.getLong()
|
|
1011
|
+
val temperature = buf.getDouble()
|
|
1012
|
+
println(f" [$timestamp] $temperature%.2f°C")
|
|
1013
|
+
count += 1
|
|
1014
|
+
}
|
|
1015
|
+
println(s"✓ Read back $count sensor readings")
|
|
1016
|
+
}.get
|
|
1017
|
+
|
|
1018
|
+
println("\n=== Pattern Use Cases ===")
|
|
1019
|
+
println("This pattern is used in:")
|
|
1020
|
+
println(" • IoT telemetry platforms (time-series databases)")
|
|
1021
|
+
println(" • High-throughput logging systems")
|
|
1022
|
+
println(" • Sensor data aggregation pipelines")
|
|
1023
|
+
println("\nKey benefits:")
|
|
1024
|
+
println(" • Automatic buffering eliminates manual position management")
|
|
1025
|
+
println(" • Integrated with Stream composition (no boilerplate)")
|
|
1026
|
+
println(" • Type-safe error handling (IOException as Sink error type)")
|
|
1027
|
+
|
|
1028
|
+
Files.delete(tempFile)
|
|
1029
|
+
}
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
Run it with:
|
|
1034
|
+
|
|
1035
|
+
```bash
|
|
1036
|
+
sbt "streams-examples/runMain sink.SinkTelemetryExample"
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
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.
|
|
1040
|
+
|
|
1041
|
+
## Running the Examples
|
|
1042
|
+
|
|
1043
|
+
All code from this guide is available as runnable examples in the `streams-examples` module.
|
|
1044
|
+
|
|
1045
|
+
Start by cloning the repository and navigating to the project:
|
|
1046
|
+
|
|
1047
|
+
```bash
|
|
1048
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
1049
|
+
cd zio-blocks
|
|
1050
|
+
```
|
|
1051
|
+
|
|
1052
|
+
Run individual examples with sbt:
|
|
1053
|
+
|
|
1054
|
+
### Basic Usage
|
|
1055
|
+
|
|
1056
|
+
This example demonstrates the most commonly used built-in sinks: `Sink.drain`, `Sink.count`, `Sink.collectAll`, `Sink.head`, `Sink.last`, and `Sink.take`:
|
|
1057
|
+
|
|
1058
|
+
```scala title="streams-examples/src/main/scala/sink/SinkBasicUsageExample.scala"
|
|
1059
|
+
package sink
|
|
1060
|
+
|
|
1061
|
+
import zio.blocks.streams.*
|
|
1062
|
+
import zio.sbt.ExprEval.show
|
|
1063
|
+
|
|
1064
|
+
object SinkBasicUsageExample extends App {
|
|
1065
|
+
println("=== Sink Basic Usage ===\n")
|
|
1066
|
+
|
|
1067
|
+
val data = Stream(1, 2, 3, 4, 5)
|
|
1068
|
+
|
|
1069
|
+
// 1. Sink.drain — discard all elements
|
|
1070
|
+
println("1. Sink.drain — discard all elements:")
|
|
1071
|
+
show(data.run(Sink.drain))
|
|
1072
|
+
|
|
1073
|
+
// 2. Sink.count — count elements
|
|
1074
|
+
println("\n2. Sink.count — count elements:")
|
|
1075
|
+
show(data.run(Sink.count))
|
|
1076
|
+
|
|
1077
|
+
// 3. Sink.collectAll — collect into Chunk
|
|
1078
|
+
println("\n3. Sink.collectAll — collect into Chunk:")
|
|
1079
|
+
show(data.run(Sink.collectAll[Int]))
|
|
1080
|
+
|
|
1081
|
+
// 4. Sink.head — first element
|
|
1082
|
+
println("\n4. Sink.head — first element:")
|
|
1083
|
+
show(data.run(Sink.head[Int]))
|
|
1084
|
+
|
|
1085
|
+
println("\n Sink.head on empty stream:")
|
|
1086
|
+
show(Stream.empty.run(Sink.head[Int]))
|
|
1087
|
+
|
|
1088
|
+
// 5. Sink.last — last element
|
|
1089
|
+
println("\n5. Sink.last — last element:")
|
|
1090
|
+
show(data.run(Sink.last[Int]))
|
|
1091
|
+
|
|
1092
|
+
// 6. Sink.take — first n elements
|
|
1093
|
+
println("\n6. Sink.take — first n elements:")
|
|
1094
|
+
show(data.run(Sink.take(3)))
|
|
1095
|
+
|
|
1096
|
+
// 7. take on a large stream (short-circuits)
|
|
1097
|
+
println("\n7. Sink.take short-circuits (only reads 3 of 1000):")
|
|
1098
|
+
show(Stream.range(0, 1000).run(Sink.take(3)))
|
|
1099
|
+
|
|
1100
|
+
// 8. Combining stream operations with sinks
|
|
1101
|
+
println("\n8. Combining stream operations with explicit sinks:")
|
|
1102
|
+
val result = Stream(1, 2, 3, 4, 5)
|
|
1103
|
+
.filter(_ % 2 == 0)
|
|
1104
|
+
.run(Sink.collectAll[Int])
|
|
1105
|
+
show(result)
|
|
1106
|
+
|
|
1107
|
+
// 9. Equivalence with convenience methods
|
|
1108
|
+
println("\n9. Stream convenience methods delegate to sinks:")
|
|
1109
|
+
show(data.runCollect == data.run(Sink.collectAll[Int]))
|
|
1110
|
+
show(data.count == data.run(Sink.count))
|
|
1111
|
+
}
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
Run this example with:
|
|
1115
|
+
|
|
1116
|
+
```bash
|
|
1117
|
+
sbt "streams-examples/runMain sink.SinkBasicUsageExample"
|
|
1118
|
+
```
|
|
1119
|
+
|
|
1120
|
+
### Aggregation and Search
|
|
1121
|
+
|
|
1122
|
+
This example shows aggregation sinks (`Sink.foldLeft`, `Sink.sumInt`, `Sink.sumDouble`) and search sinks (`Sink.exists`, `Sink.forall`, `Sink.find`, `Sink.foreach`):
|
|
1123
|
+
|
|
1124
|
+
```scala title="streams-examples/src/main/scala/sink/SinkAggregationExample.scala"
|
|
1125
|
+
package sink
|
|
1126
|
+
|
|
1127
|
+
import zio.blocks.streams.*
|
|
1128
|
+
import zio.sbt.ExprEval.show
|
|
1129
|
+
|
|
1130
|
+
object SinkAggregationExample extends App {
|
|
1131
|
+
println("=== Sink Aggregation and Search ===\n")
|
|
1132
|
+
|
|
1133
|
+
// 1. foldLeft — general accumulation
|
|
1134
|
+
println("1. Sink.foldLeft — general accumulation:")
|
|
1135
|
+
val sum = Stream(1, 2, 3, 4, 5).run(Sink.foldLeft(0)(_ + _))
|
|
1136
|
+
show(sum)
|
|
1137
|
+
|
|
1138
|
+
println("\n foldLeft with string concatenation:")
|
|
1139
|
+
val concat = Stream("a", "b", "c").run(Sink.foldLeft("")(_ + _))
|
|
1140
|
+
show(concat)
|
|
1141
|
+
// 2. sumInt — typed numeric sum
|
|
1142
|
+
println("\n2. Sink.sumInt — returns Long to avoid overflow:")
|
|
1143
|
+
val intSum = Stream(1, 2, 3, 4, 5).run(Sink.sumInt)
|
|
1144
|
+
show(intSum)
|
|
1145
|
+
|
|
1146
|
+
// 3. sumDouble — typed floating point sum
|
|
1147
|
+
println("\n3. Sink.sumDouble:")
|
|
1148
|
+
val doubleSum = Stream(1.5, 2.5, 3.0).run(Sink.sumDouble)
|
|
1149
|
+
show(doubleSum)
|
|
1150
|
+
|
|
1151
|
+
// 4. exists — short-circuits on first match
|
|
1152
|
+
println("\n4. Sink.exists — short-circuits on first match:")
|
|
1153
|
+
val hasNegative = Stream(1, 2, -3, 4).run(Sink.exists[Int](_ < 0))
|
|
1154
|
+
show(hasNegative)
|
|
1155
|
+
|
|
1156
|
+
val noNegative = Stream(1, 2, 3, 4).run(Sink.exists[Int](_ < 0))
|
|
1157
|
+
show(noNegative)
|
|
1158
|
+
|
|
1159
|
+
// 5. forall — all elements must match
|
|
1160
|
+
println("\n5. Sink.forall — all elements must match:")
|
|
1161
|
+
val allPositive = Stream(1, 2, 3).run(Sink.forall[Int](_ > 0))
|
|
1162
|
+
show(allPositive)
|
|
1163
|
+
|
|
1164
|
+
val notAllPositive = Stream(1, -2, 3).run(Sink.forall[Int](_ > 0))
|
|
1165
|
+
show(notAllPositive)
|
|
1166
|
+
|
|
1167
|
+
// 6. find — first element matching predicate
|
|
1168
|
+
println("\n6. Sink.find — first matching element:")
|
|
1169
|
+
val firstEven = Stream(1, 3, 4, 6, 8).run(Sink.find[Int](_ % 2 == 0))
|
|
1170
|
+
show(firstEven)
|
|
1171
|
+
|
|
1172
|
+
val noMatch = Stream(1, 3, 5, 7).run(Sink.find[Int](_ % 2 == 0))
|
|
1173
|
+
show(noMatch)
|
|
1174
|
+
|
|
1175
|
+
// 7. foreach — side effects
|
|
1176
|
+
println("\n7. Sink.foreach — apply side effects:")
|
|
1177
|
+
val items = scala.collection.mutable.Buffer[String]()
|
|
1178
|
+
val result = Stream("x", "y", "z").run(Sink.foreach[String](s => items += s))
|
|
1179
|
+
show(result)
|
|
1180
|
+
show(items.toList)
|
|
1181
|
+
|
|
1182
|
+
// 8. Complex aggregation: combine foldLeft with map
|
|
1183
|
+
println("\n8. Complex aggregation — average via foldLeft + map:")
|
|
1184
|
+
val average = Sink
|
|
1185
|
+
.foldLeft[Int, (Int, Int)]((0, 0)) { case ((sum, count), x) =>
|
|
1186
|
+
(sum + x, count + 1)
|
|
1187
|
+
}
|
|
1188
|
+
.map { case (sum, count) =>
|
|
1189
|
+
if (count == 0) 0.0 else sum.toDouble / count
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
val avg = Stream(10, 20, 30, 40).run(average)
|
|
1193
|
+
show(avg)
|
|
1194
|
+
}
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
Run this example with:
|
|
1198
|
+
|
|
1199
|
+
```bash
|
|
1200
|
+
sbt "streams-examples/runMain sink.SinkAggregationExample"
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
### Transformations and Composition
|
|
1204
|
+
|
|
1205
|
+
This example demonstrates `Sink#contramap`, `Sink#map`, `Sink#mapError`, `Sink.fail`, `Sink.create`, and `Pipeline#andThenSink`:
|
|
1206
|
+
|
|
1207
|
+
```scala title="streams-examples/src/main/scala/sink/SinkTransformationExample.scala"
|
|
1208
|
+
package sink
|
|
1209
|
+
|
|
1210
|
+
import zio.blocks.streams.*
|
|
1211
|
+
import zio.sbt.ExprEval.show
|
|
1212
|
+
|
|
1213
|
+
object SinkTransformationExample extends App {
|
|
1214
|
+
println("=== Sink Transformations and Composition ===\n")
|
|
1215
|
+
|
|
1216
|
+
// 1. contramap — pre-process input
|
|
1217
|
+
println("1. Sink.contramap — pre-process input elements:")
|
|
1218
|
+
val stringLengthSum: Sink[Nothing, String, Long] =
|
|
1219
|
+
Sink.sumInt.contramap[Int, String](_.length)
|
|
1220
|
+
|
|
1221
|
+
show(Stream("hello", "world").run(stringLengthSum))
|
|
1222
|
+
|
|
1223
|
+
// 2. contramap — change element type
|
|
1224
|
+
println("\n2. contramap to convert types:")
|
|
1225
|
+
val parseInts: Sink[Nothing, String, Long] =
|
|
1226
|
+
Sink.sumInt.contramap[Int, String](_.toInt)
|
|
1227
|
+
|
|
1228
|
+
show(Stream("10", "20", "30").run(parseInts))
|
|
1229
|
+
|
|
1230
|
+
// 3. map — transform result
|
|
1231
|
+
println("\n3. Sink.map — transform the result:")
|
|
1232
|
+
val countFormatted: Sink[Nothing, Any, String] =
|
|
1233
|
+
Sink.count.map(n => s"Processed $n elements")
|
|
1234
|
+
|
|
1235
|
+
show(Stream(1, 2, 3).run(countFormatted))
|
|
1236
|
+
|
|
1237
|
+
// 4. Chaining contramap + map
|
|
1238
|
+
println("\n4. Chaining contramap + map:")
|
|
1239
|
+
val pipeline = Sink.sumInt
|
|
1240
|
+
.contramap[Int, String](_.length)
|
|
1241
|
+
.map(total => s"Total chars: $total")
|
|
1242
|
+
|
|
1243
|
+
show(Stream("hi", "hello").run(pipeline))
|
|
1244
|
+
|
|
1245
|
+
// 5. mapError — transform error channel
|
|
1246
|
+
println("\n5. Sink.mapError — transform errors:")
|
|
1247
|
+
|
|
1248
|
+
sealed trait AppError
|
|
1249
|
+
case class ParseError(msg: String) extends AppError
|
|
1250
|
+
|
|
1251
|
+
val failingSink = Sink.fail("raw error").mapError[AppError](msg => ParseError(msg))
|
|
1252
|
+
show(Stream(1).run(failingSink))
|
|
1253
|
+
|
|
1254
|
+
// 6. fail — immediately fail
|
|
1255
|
+
println("\n6. Sink.fail — immediate failure:")
|
|
1256
|
+
show(Stream(1, 2, 3).run(Sink.fail("error")))
|
|
1257
|
+
|
|
1258
|
+
// 7. Pipeline.andThenSink integration
|
|
1259
|
+
println("\n7. Pipeline.andThenSink — pipeline pre-processes before sink:")
|
|
1260
|
+
val cleanAndCollect =
|
|
1261
|
+
Pipeline
|
|
1262
|
+
.map[String, String](_.trim.toLowerCase)
|
|
1263
|
+
.andThenSink(Sink.collectAll[String])
|
|
1264
|
+
|
|
1265
|
+
show(Stream(" Hello ", " WORLD ").run(cleanAndCollect))
|
|
1266
|
+
|
|
1267
|
+
// 8. Equivalence: via + run == andThenSink + run
|
|
1268
|
+
println("\n8. Equivalence law: via + run == andThenSink + run:")
|
|
1269
|
+
val pipe = Pipeline.filter[Int](_ > 2).andThen(Pipeline.map[Int, Int](_ * 10))
|
|
1270
|
+
val source = Stream(1, 2, 3, 4, 5)
|
|
1271
|
+
|
|
1272
|
+
val viaResult = source.via(pipe).run(Sink.collectAll[Int])
|
|
1273
|
+
val sinkResult = source.run(pipe.andThenSink(Sink.collectAll[Int]))
|
|
1274
|
+
show(viaResult)
|
|
1275
|
+
show(sinkResult)
|
|
1276
|
+
|
|
1277
|
+
// 9. Composing multiple transformations into a reusable sink
|
|
1278
|
+
println("\n9. Reusable composed sink:")
|
|
1279
|
+
case class Metric(name: String, value: Double)
|
|
1280
|
+
|
|
1281
|
+
val metricSumSink: Sink[Nothing, Metric, Double] =
|
|
1282
|
+
Sink.foldLeft(0.0)((acc, m: Metric) => acc + m.value)
|
|
1283
|
+
|
|
1284
|
+
val metrics = Stream(
|
|
1285
|
+
Metric("cpu", 45.0),
|
|
1286
|
+
Metric("cpu", 67.0),
|
|
1287
|
+
Metric("cpu", 23.0)
|
|
1288
|
+
)
|
|
1289
|
+
show(metrics.run(metricSumSink))
|
|
1290
|
+
|
|
1291
|
+
val metricAvgSink: Sink[Nothing, Metric, Double] =
|
|
1292
|
+
Sink
|
|
1293
|
+
.foldLeft[Metric, (Double, Int)]((0.0, 0)) { case ((sum, count), m) =>
|
|
1294
|
+
(sum + m.value, count + 1)
|
|
1295
|
+
}
|
|
1296
|
+
.map { case (sum, count) => if (count == 0) 0.0 else sum / count }
|
|
1297
|
+
|
|
1298
|
+
show(metrics.run(metricAvgSink))
|
|
1299
|
+
}
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
Run this example with:
|
|
1303
|
+
|
|
1304
|
+
```bash
|
|
1305
|
+
sbt "streams-examples/runMain sink.SinkTransformationExample"
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
### Async Sinks End to End
|
|
1309
|
+
|
|
1310
|
+
This example builds a custom mean sink with `Sink.createBoth` — a `while` loop for the synchronous drain, an `Async#flatMap` recursion for the asynchronous one — then drives it and `Sink.existsAsync` through `Stream#runAsync`, unwrapping each `Either` at the end. It is JVM-only, because it finishes with `.block` to turn the `Async` into a value for `main`:
|
|
1311
|
+
|
|
1312
|
+
```scala title="streams-examples/src/main/scala/sink/SinkAsyncExample.scala"
|
|
1313
|
+
package sink
|
|
1314
|
+
|
|
1315
|
+
import zio.blocks.async._
|
|
1316
|
+
import zio.blocks.streams._
|
|
1317
|
+
import zio.blocks.streams.io.Reader
|
|
1318
|
+
|
|
1319
|
+
/**
|
|
1320
|
+
* A custom sink built with `Sink.createBoth`, plus one of the `*Async`
|
|
1321
|
+
* combinators, both driven from the cross-platform asynchronous terminal
|
|
1322
|
+
* `Stream#runAsync`.
|
|
1323
|
+
*
|
|
1324
|
+
* `createBoth` supplies two independent drains. The terminal picks exactly one:
|
|
1325
|
+
* `runAsync` takes the asynchronous callback, and the JVM-only blocking `run`
|
|
1326
|
+
* takes the synchronous one. Both must agree on what they consume and what they
|
|
1327
|
+
* return.
|
|
1328
|
+
*
|
|
1329
|
+
* JVM only, because it ends in `.block` to turn the `Async` into a value for
|
|
1330
|
+
* `main`.
|
|
1331
|
+
*/
|
|
1332
|
+
object SinkAsyncExample {
|
|
1333
|
+
|
|
1334
|
+
/**
|
|
1335
|
+
* Mean of an `Int` lane. The synchronous callback is a plain `while` loop
|
|
1336
|
+
* over `readInt`, which allocates no `Async` per element; the asynchronous
|
|
1337
|
+
* callback threads the same accumulator through `flatMap`.
|
|
1338
|
+
*/
|
|
1339
|
+
val meanCelsius: Sink[Nothing, Int, Double] =
|
|
1340
|
+
Sink.createBoth[Nothing, Int, Double](
|
|
1341
|
+
sync = { (reader: Reader.SyncReader[Int]) =>
|
|
1342
|
+
var sum = 0L
|
|
1343
|
+
var count = 0L
|
|
1344
|
+
var value = reader.readInt(Long.MinValue)
|
|
1345
|
+
while (value != Long.MinValue) {
|
|
1346
|
+
sum += value
|
|
1347
|
+
count += 1L
|
|
1348
|
+
value = reader.readInt(Long.MinValue)
|
|
1349
|
+
}
|
|
1350
|
+
mean(sum, count)
|
|
1351
|
+
},
|
|
1352
|
+
async = { (reader: Reader.AsyncReader[Int]) =>
|
|
1353
|
+
def loop(sum: Long, count: Long): Async[Double] =
|
|
1354
|
+
reader.readInt(Long.MinValue).flatMap { value =>
|
|
1355
|
+
if (value == Long.MinValue) Async.succeed(mean(sum, count))
|
|
1356
|
+
else loop(sum + value, count + 1L)
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
loop(0L, 0L)
|
|
1360
|
+
}
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
private def mean(sum: Long, count: Long): Double =
|
|
1364
|
+
if (count == 0L) 0.0 else sum.toDouble / count.toDouble
|
|
1365
|
+
|
|
1366
|
+
def main(args: Array[String]): Unit = {
|
|
1367
|
+
val celsius: Stream[Nothing, Int] = Stream(12, 7, 30, 21, 5)
|
|
1368
|
+
|
|
1369
|
+
// Nothing has run yet: both terminals are descriptions.
|
|
1370
|
+
val average: Async[Either[Nothing, Double]] = celsius.runAsync(meanCelsius)
|
|
1371
|
+
|
|
1372
|
+
// `existsAsync` runs at most one predicate effect at a time and stops at
|
|
1373
|
+
// the first `true`; 30 matches, so 21 and 5 are never pulled.
|
|
1374
|
+
val tooHot: Async[Either[Nothing, Boolean]] =
|
|
1375
|
+
celsius.runAsync(Sink.existsAsync[Int](c => Async.succeed(c > 25)))
|
|
1376
|
+
|
|
1377
|
+
// `.block` parks the calling thread until the value arrives. It belongs
|
|
1378
|
+
// here, at the edge of a JVM `main`, and nowhere else.
|
|
1379
|
+
report("createBoth mean", average.block)
|
|
1380
|
+
report("existsAsync (> 25)", tooHot.block)
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
private def report[E, Z](label: String, result: Either[E, Z]): Unit =
|
|
1384
|
+
result match {
|
|
1385
|
+
case Right(value) => println(s"$label -> $value")
|
|
1386
|
+
case Left(error) => println(s"$label -> typed error: $error")
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
Run this example with:
|
|
1392
|
+
|
|
1393
|
+
```bash
|
|
1394
|
+
sbt "streams-examples/runMain sink.SinkAsyncExample"
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
## See Also
|
|
1398
|
+
|
|
1399
|
+
- [Asynchronous Stream Execution](../execution-and-compatibility/async-execution.md#async-terminals) — the terminals that select a sink's asynchronous drain, and the `Async[Either[E, Z]]` convention
|
|
1400
|
+
- [Platform Differences](../execution-and-compatibility/platform-differences.md#sinkcreate-and-custom-sinks) — why `Sink.create` is JVM-only and what replaces it in shared source
|
|
1401
|
+
- [Reader](../primitives/reader.md#the-reader-union) — the two reader kinds a sink drains, from the reader's side
|
|
1402
|
+
- [Zero-Boxing Streams](../execution-and-compatibility/zero-boxing.md) — how a primitive lane is chosen, and the per-lane end-of-stream table
|
|
1403
|
+
- [Stream](./stream.md) — the producer a sink consumes
|
|
1404
|
+
- [Pipeline](./pipeline.md) — transformations that can be attached to a sink's input
|