@zio.dev/zio-blocks 0.0.33 → 0.0.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +292 -50
- 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} +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} +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 +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.md → schema/schema.md} +12 -0
- 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 +5 -19
- package/sidebars.js +238 -43
- 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,1045 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: writer
|
|
3
|
+
title: "Writer"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
`Writer[-Elem]` is a **push-based sink for elements** that accepts values one at a time until closed or filled. It is the push-based counterpart to `Reader[+Elem]` (which pulls). Elements are written on demand by the producer, making it ideal for streaming, buffering, and integration with I/O subsystems. The fundamental operations are `write(elem): Boolean` — pushes an element and returns success or closure — and `close()` — signals the end of writing and releases resources.
|
|
7
|
+
|
|
8
|
+
`Writer[-Elem]` has these key properties:
|
|
9
|
+
|
|
10
|
+
- **Lazy and Push-Based** — nothing happens until the producer calls `write()`
|
|
11
|
+
- **Non-Thread-Safe** — designed for single-threaded production; concurrent access requires external synchronization
|
|
12
|
+
- **Explicit Closure Signal** — returns `false` when closed (clean closure) or throws when error-closed
|
|
13
|
+
|
|
14
|
+
Here is the structural shape of the `Writer` type:
|
|
15
|
+
|
|
16
|
+
```scala
|
|
17
|
+
abstract class Writer[-Elem] {
|
|
18
|
+
def write(a: Elem): Boolean
|
|
19
|
+
def close(): Unit
|
|
20
|
+
def isClosed: Boolean
|
|
21
|
+
|
|
22
|
+
// concrete defaults for fail() and writeable()
|
|
23
|
+
def fail(error: Throwable): Unit = close()
|
|
24
|
+
def writeable(): Boolean = !isClosed
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Motivation
|
|
29
|
+
|
|
30
|
+
Imagine you're building a data pipeline where a producer feeds items to a bounded sink. The producer doesn't control the sink's internal state—how much capacity remains, whether it's busy, or if it's permanently closed. You need to know before each write: Is the sink ready? Did the write succeed? Is the sink closed?
|
|
31
|
+
|
|
32
|
+
With Java's `OutputStream`, you call `write()` and either it succeeds (void return) or throws an exception. This leaves ambiguity: Was the exception transient (try again later) or permanent (the stream is done)? If the buffer fills, the thread blocks—but you don't know how long, or even that it will block beforehand. There's no way to check capacity upfront, so you're forced to either over-allocate buffers (wasting memory) or catch exceptions and guess the right strategy.
|
|
33
|
+
|
|
34
|
+
`Writer` makes the state explicit and non-throwing. You check readiness with `writeable()`, then push with `write()`, which returns a `Boolean` indicating success or closure. The protocol is clear and exception-free: when `write()` returns `false`, the sink is permanently closed and you should stop.
|
|
35
|
+
|
|
36
|
+
## Quick Showcase
|
|
37
|
+
|
|
38
|
+
Here's how to create and push elements to a `Writer`:
|
|
39
|
+
|
|
40
|
+
```scala
|
|
41
|
+
import zio.blocks.streams.io.Writer
|
|
42
|
+
import scala.collection.mutable.Buffer
|
|
43
|
+
|
|
44
|
+
val collected = Buffer[Int]()
|
|
45
|
+
// collected: Buffer[Int] = ArrayBuffer(10, 20, 30, 40, 50)
|
|
46
|
+
val w = new Writer[Int] {
|
|
47
|
+
private var closed = false
|
|
48
|
+
|
|
49
|
+
def isClosed = closed
|
|
50
|
+
def write(a: Int) = {
|
|
51
|
+
if (!closed) { collected += a; true }
|
|
52
|
+
else false
|
|
53
|
+
}
|
|
54
|
+
def close() = { closed = true }
|
|
55
|
+
override def fail(error: Throwable) = close()
|
|
56
|
+
override def writeable() = !isClosed
|
|
57
|
+
}
|
|
58
|
+
// w: Writer[Int] = repl.MdocSession$MdocApp0$$anon$2@350ce732
|
|
59
|
+
|
|
60
|
+
// Push elements, checking writeable() before each write
|
|
61
|
+
def pushAll(elements: List[Int]): Unit = {
|
|
62
|
+
elements match {
|
|
63
|
+
case Nil => ()
|
|
64
|
+
case head :: tail =>
|
|
65
|
+
if (w.writeable() && w.write(head)) pushAll(tail)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pushAll(List(10, 20, 30, 40, 50))
|
|
70
|
+
w.close()
|
|
71
|
+
|
|
72
|
+
println(s"Collected: $collected")
|
|
73
|
+
// Collected: ArrayBuffer(10, 20, 30, 40, 50)
|
|
74
|
+
println(s"Writable after close: ${w.writeable()}")
|
|
75
|
+
// Writable after close: false
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Writing and Closure
|
|
79
|
+
|
|
80
|
+
The fundamental protocol is: call `write(element)` to push an element. It returns `true` on success, `false` only when the writer is **closed** (not when the buffer is full). Once `write()` returns `false`, the writer is permanently closed—all further writes return `false`. There is no recovery.
|
|
81
|
+
|
|
82
|
+
```scala
|
|
83
|
+
import zio.blocks.streams.io.Writer
|
|
84
|
+
|
|
85
|
+
val w = Writer.single[Int]
|
|
86
|
+
// w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@78b107f
|
|
87
|
+
println(s"First write: ${w.write(42)}") // true (accepted)
|
|
88
|
+
// First write: true
|
|
89
|
+
println(s"Second write: ${w.write(99)}") // false (writer auto-closed after one element)
|
|
90
|
+
// Second write: false
|
|
91
|
+
println(s"Third write: ${w.write(77)}") // false (still closed)
|
|
92
|
+
// Third write: false
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Capacity and Buffering
|
|
96
|
+
|
|
97
|
+
The default `writeable()` method returns `!isClosed`—it only tells you if the writer is closed, not whether the buffer has space. Bounded implementations can override `writeable()` to reflect remaining capacity, but this is not guaranteed by the interface. The important distinction:
|
|
98
|
+
|
|
99
|
+
- **`writeable()` returns `false`**: the writer is closed (permanent state)
|
|
100
|
+
- **`writeable()` returns `true` but `write()` would block**: the buffer is full but not closed; bounded implementations block the calling thread until space becomes available (they don't return `false`)
|
|
101
|
+
|
|
102
|
+
Implementations like `ByteBufferWriter` auto-close when the buffer fills, turning the full state into closure. Others may block indefinitely waiting for space.
|
|
103
|
+
|
|
104
|
+
## Error Handling
|
|
105
|
+
|
|
106
|
+
When the writer encounters an error, signal it with `fail(error)`. By default, `fail()` closes the writer; all subsequent `write()` calls return `false`.
|
|
107
|
+
|
|
108
|
+
If you override `fail()` to store the error internally, `write()` will throw it on the next call:
|
|
109
|
+
|
|
110
|
+
```scala
|
|
111
|
+
import zio.blocks.streams.io.Writer
|
|
112
|
+
|
|
113
|
+
class ErrorStoringWriter extends Writer[Int] {
|
|
114
|
+
private var closed = false
|
|
115
|
+
private var storedError: Option[Throwable] = None
|
|
116
|
+
|
|
117
|
+
def isClosed = closed
|
|
118
|
+
def write(a: Int): Boolean = {
|
|
119
|
+
if (storedError.isDefined) throw storedError.get
|
|
120
|
+
if (closed) false else true
|
|
121
|
+
}
|
|
122
|
+
def close() = { closed = true }
|
|
123
|
+
override def fail(error: Throwable) = {
|
|
124
|
+
storedError = Some(error)
|
|
125
|
+
closed = true
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
val w = new ErrorStoringWriter()
|
|
130
|
+
// w: ErrorStoringWriter = repl.MdocSession$MdocApp9$ErrorStoringWriter@35e00b45
|
|
131
|
+
w.fail(new Exception("Stream error"))
|
|
132
|
+
try {
|
|
133
|
+
w.write(42) // throws the stored error
|
|
134
|
+
} catch {
|
|
135
|
+
case e: Exception => println(s"Caught: ${e.getMessage}")
|
|
136
|
+
}
|
|
137
|
+
// Caught: Stream error
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This gives you optional error propagation: use the default `fail()` for silent closure, or override it to propagate errors as exceptions.
|
|
141
|
+
|
|
142
|
+
## Construction
|
|
143
|
+
|
|
144
|
+
Writers are created using factory methods on the companion object, from adapters wrapping Java I/O, or by direct subclassing for custom implementations:
|
|
145
|
+
|
|
146
|
+
### Creating Predefined Writers
|
|
147
|
+
|
|
148
|
+
`Writer.closed` — A pre-closed writer that rejects all writes. Useful as a base case for empty streams:
|
|
149
|
+
|
|
150
|
+
```scala
|
|
151
|
+
object Writer {
|
|
152
|
+
def closed: Writer[Any]
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Create a pre-closed writer that rejects all writes:
|
|
157
|
+
|
|
158
|
+
```scala
|
|
159
|
+
import zio.blocks.streams.io.Writer
|
|
160
|
+
|
|
161
|
+
val w = Writer.closed
|
|
162
|
+
// w: Writer[Any] = zio.blocks.streams.io.Writer$$anon$1@603b3a16
|
|
163
|
+
println(w.write(42)) // false (closed)
|
|
164
|
+
// false
|
|
165
|
+
println(w.isClosed) // true
|
|
166
|
+
// true
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Single Element
|
|
170
|
+
|
|
171
|
+
`Writer.single` — Creates a writer that accepts exactly one element, then auto-closes. The dual of `Reader.single`:
|
|
172
|
+
|
|
173
|
+
```scala
|
|
174
|
+
object Writer {
|
|
175
|
+
def single[Elem]: Writer[Elem]
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Create a writer that accepts exactly one element, then auto-closes:
|
|
180
|
+
|
|
181
|
+
```scala
|
|
182
|
+
import zio.blocks.streams.io.Writer
|
|
183
|
+
|
|
184
|
+
val w = Writer.single[Int]
|
|
185
|
+
// w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@20f8ee95
|
|
186
|
+
println(w.write(42)) // true
|
|
187
|
+
// true
|
|
188
|
+
println(w.write(99)) // false (already accepted one element)
|
|
189
|
+
// false
|
|
190
|
+
println(w.isClosed) // true
|
|
191
|
+
// true
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Limited Capacity
|
|
195
|
+
|
|
196
|
+
`Writer.limited` — Creates a writer that accepts at most `n` elements from `inner`, then becomes closed. The dual of `Stream.take`. If `inner` closes before `n` elements are accepted, the limited writer also closes immediately without consuming the remaining capacity.
|
|
197
|
+
|
|
198
|
+
:::note
|
|
199
|
+
The inner writer is not automatically closed—only the limited wrapper's `isClosed` returns `true` when the limit is reached. The inner writer stays open until someone explicitly calls `close()`.
|
|
200
|
+
:::
|
|
201
|
+
|
|
202
|
+
```scala
|
|
203
|
+
object Writer {
|
|
204
|
+
def limited[Elem](inner: Writer[Elem], n: Long): Writer[Elem]
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Limit a writer to accept at most n elements:
|
|
209
|
+
|
|
210
|
+
```scala
|
|
211
|
+
import zio.blocks.streams.io.Writer
|
|
212
|
+
import scala.collection.mutable.Buffer
|
|
213
|
+
|
|
214
|
+
val collected = Buffer[Int]()
|
|
215
|
+
// collected: Buffer[Int] = ArrayBuffer(1, 2)
|
|
216
|
+
val inner = new Writer[Int] {
|
|
217
|
+
def isClosed = false
|
|
218
|
+
def write(a: Int) = { collected += a; true }
|
|
219
|
+
def close() = ()
|
|
220
|
+
}
|
|
221
|
+
// inner: Writer[Int] = repl.MdocSession$MdocApp19$$anon$23@73ed153a
|
|
222
|
+
|
|
223
|
+
val limited = Writer.limited(inner, 2)
|
|
224
|
+
// limited: Writer[Int] = zio.blocks.streams.io.Writer$LimitedWriter@6e804688
|
|
225
|
+
println(limited.write(1)) // true
|
|
226
|
+
// true
|
|
227
|
+
println(limited.write(2)) // true (space available)
|
|
228
|
+
// true
|
|
229
|
+
println(limited.write(3)) // false (limit of 2 reached)
|
|
230
|
+
// false
|
|
231
|
+
println(s"Collected: $collected") // Collected: Buffer(1, 2)
|
|
232
|
+
// Collected: ArrayBuffer(1, 2)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### I/O Adapters
|
|
236
|
+
|
|
237
|
+
`Writer.fromOutputStream` — Wraps a `java.io.OutputStream` as a `Writer[Byte]`. Calling `close()` flushes and closes the underlying stream:
|
|
238
|
+
|
|
239
|
+
```scala
|
|
240
|
+
object Writer {
|
|
241
|
+
def fromOutputStream(os: OutputStream): Writer[Byte]
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`Writer.fromWriter` — Wraps a `java.io.Writer` as a `Writer[Char]`. Calling `close()` flushes and closes the underlying writer:
|
|
246
|
+
|
|
247
|
+
```scala
|
|
248
|
+
object Writer {
|
|
249
|
+
def fromWriter(w: java.io.Writer): Writer[Char]
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## Core Operations
|
|
254
|
+
|
|
255
|
+
The fundamental operations on `Writer` cover pushing elements one at a time, bulk operations, specialized writes for primitives, and state checks:
|
|
256
|
+
|
|
257
|
+
### Writing Elements
|
|
258
|
+
|
|
259
|
+
`Writer#write` — Pushes one element to the writer. Returns `true` on success, `false` if the writer is closed and cannot accept more elements. Throws if the writer was closed with an error via `Writer#fail`:
|
|
260
|
+
|
|
261
|
+
```scala
|
|
262
|
+
abstract class Writer[-Elem] {
|
|
263
|
+
def write(a: Elem): Boolean
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Write elements and observe the return value indicating success or closure:
|
|
268
|
+
|
|
269
|
+
```scala
|
|
270
|
+
import zio.blocks.streams.io.Writer
|
|
271
|
+
|
|
272
|
+
val w = Writer.single[Int]
|
|
273
|
+
// w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@5855f9a7
|
|
274
|
+
val result1 = w.write(42)
|
|
275
|
+
// result1: Boolean = true
|
|
276
|
+
val result2 = w.write(99) // false, already closed
|
|
277
|
+
// result2: Boolean = false
|
|
278
|
+
println(s"First: $result1, Second: $result2")
|
|
279
|
+
// First: true, Second: false
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Bulk Writing
|
|
283
|
+
|
|
284
|
+
`Writer#writeAll` — Writes every element in a chunk. Returns the suffix not delivered. If the writer is already closed, returns the entire chunk. Exceptions from individual writes propagate to the caller:
|
|
285
|
+
|
|
286
|
+
```scala
|
|
287
|
+
abstract class Writer[-Elem] {
|
|
288
|
+
def writeAll[Elem1 <: Elem](chunk: Chunk[Elem1]): Chunk[Elem1]
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Write a chunk and observe how many elements were delivered:
|
|
293
|
+
|
|
294
|
+
```scala
|
|
295
|
+
import zio.blocks.streams.io.Writer
|
|
296
|
+
import zio.blocks.chunk.Chunk
|
|
297
|
+
|
|
298
|
+
val w = Writer.single[Int]
|
|
299
|
+
// w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@4130dfc
|
|
300
|
+
val chunk = Chunk(1, 2, 3)
|
|
301
|
+
// chunk: Chunk[Int] = IndexedSeq(1, 2, 3)
|
|
302
|
+
val remaining = w.writeAll(chunk)
|
|
303
|
+
// remaining: Chunk[Int] = IndexedSeq(2, 3)
|
|
304
|
+
println(s"Remaining: $remaining") // Chunk(2, 3)
|
|
305
|
+
// Remaining: Chunk(2,3)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Specialized Writes
|
|
309
|
+
|
|
310
|
+
For primitive types, specialized write methods avoid boxing by using subtype witnesses.
|
|
311
|
+
|
|
312
|
+
`writeInt` — Specialized `Int` write. Requires implicit evidence that `Int` is a subtype of `Elem`:
|
|
313
|
+
|
|
314
|
+
```scala
|
|
315
|
+
abstract class Writer[-Elem] {
|
|
316
|
+
def writeInt(value: Int)(using Int <:< Elem): Boolean
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
`writeLong` — Specialized `Long` write:
|
|
321
|
+
|
|
322
|
+
```scala
|
|
323
|
+
abstract class Writer[-Elem] {
|
|
324
|
+
def writeLong(value: Long)(using Long <:< Elem): Boolean
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
`writeFloat` — Specialized `Float` write:
|
|
329
|
+
|
|
330
|
+
```scala
|
|
331
|
+
abstract class Writer[-Elem] {
|
|
332
|
+
def writeFloat(value: Float)(using Float <:< Elem): Boolean
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
`writeDouble` — Specialized `Double` write:
|
|
337
|
+
|
|
338
|
+
```scala
|
|
339
|
+
abstract class Writer[-Elem] {
|
|
340
|
+
def writeDouble(value: Double)(using Double <:< Elem): Boolean
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### Byte and Character Writes
|
|
345
|
+
|
|
346
|
+
`writeByte` — Specialized byte write. Avoids boxing when `Elem = Byte`. Requires evidence that `Byte` is a subtype of `Elem`:
|
|
347
|
+
|
|
348
|
+
```scala
|
|
349
|
+
abstract class Writer[-Elem] {
|
|
350
|
+
def writeByte(b: Byte)(using Byte <:< Elem): Boolean
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`writeBytes` — Blocking bulk byte write. Calls `writeByte` for each byte in `buf[offset, offset+len)`, stopping early if the channel closes. Returns the number of bytes successfully written:
|
|
355
|
+
|
|
356
|
+
```scala
|
|
357
|
+
abstract class Writer[-Elem] {
|
|
358
|
+
def writeBytes(buf: Array[Byte], offset: Int, len: Int)(using Byte <:< Elem): Int
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
`writeChar` — Specialized `Char` write. Requires evidence that `Char` is a subtype of `Elem`:
|
|
363
|
+
|
|
364
|
+
```scala
|
|
365
|
+
abstract class Writer[-Elem] {
|
|
366
|
+
def writeChar(value: Char)(using Char <:< Elem): Boolean
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`writeShort` — Specialized `Short` write. Requires evidence that `Short` is a subtype of `Elem`:
|
|
371
|
+
|
|
372
|
+
```scala
|
|
373
|
+
abstract class Writer[-Elem] {
|
|
374
|
+
def writeShort(value: Short)(using Short <:< Elem): Boolean
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`writeBoolean` — Specialized `Boolean` write. Requires evidence that `Boolean` is a subtype of `Elem`:
|
|
379
|
+
|
|
380
|
+
```scala
|
|
381
|
+
abstract class Writer[-Elem] {
|
|
382
|
+
def writeBoolean(value: Boolean)(using Boolean <:< Elem): Boolean
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### State Checks
|
|
387
|
+
|
|
388
|
+
`Writer#isClosed` — Returns `true` if the writer is closed. Monotone: once `true`, never returns `false`:
|
|
389
|
+
|
|
390
|
+
```scala
|
|
391
|
+
abstract class Writer[-Elem] {
|
|
392
|
+
def isClosed: Boolean
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`writeable` — Returns `true` if the next `write()` would accept a value without blocking (space is available and the writer is not closed). Default returns `!isClosed`. Buffered writers override for accuracy. Note: the analogous method on `Reader` is named `readable()`, not `writable()`.
|
|
397
|
+
|
|
398
|
+
```scala
|
|
399
|
+
abstract class Writer[-Elem] {
|
|
400
|
+
def writeable(): Boolean
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Check writer capacity before writing:
|
|
405
|
+
|
|
406
|
+
```scala
|
|
407
|
+
import zio.blocks.streams.io.Writer
|
|
408
|
+
|
|
409
|
+
val w = Writer.single[Int]
|
|
410
|
+
// w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@35d64778
|
|
411
|
+
println(w.writeable()) // true
|
|
412
|
+
// true
|
|
413
|
+
w.write(42)
|
|
414
|
+
// res30: Boolean = true
|
|
415
|
+
println(w.writeable()) // false (closed after accepting one)
|
|
416
|
+
// false
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Composition
|
|
420
|
+
|
|
421
|
+
Writers can be concatenated to chain multiple sinks together, or transformed to adapt their input types:
|
|
422
|
+
|
|
423
|
+
### Concatenation
|
|
424
|
+
|
|
425
|
+
`Writer#concat` — Returns a `Writer` that writes to `this` until it closes, then transparently switches to `next`. If `this` closes with an error, the error is propagated immediately without consulting `next`. The dual of `Reader#concat`:
|
|
426
|
+
|
|
427
|
+
```scala
|
|
428
|
+
abstract class Writer[-Elem] {
|
|
429
|
+
def concat[Elem1 <: Elem](next: => Writer[Elem1]): Writer[Elem1]
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
`Writer#++` — Alias for `Writer#concat`. Syntactic sugar for composing writers:
|
|
434
|
+
|
|
435
|
+
```scala
|
|
436
|
+
abstract class Writer[-Elem] {
|
|
437
|
+
def ++[Elem1 <: Elem](next: => Writer[Elem1]): Writer[Elem1]
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Here is how concatenation switches to the next writer when the first closes:
|
|
442
|
+
|
|
443
|
+
```scala
|
|
444
|
+
import zio.blocks.streams.io.Writer
|
|
445
|
+
import scala.collection.mutable
|
|
446
|
+
|
|
447
|
+
val collected = mutable.ArrayBuffer[Int]()
|
|
448
|
+
// collected: ArrayBuffer[Int] = ArrayBuffer(5, 200)
|
|
449
|
+
val w1 = new Writer[Int] {
|
|
450
|
+
def isClosed = false
|
|
451
|
+
def write(a: Int) = {
|
|
452
|
+
if (a < 10) { collected += a; true; }
|
|
453
|
+
else false
|
|
454
|
+
}
|
|
455
|
+
def close() = ()
|
|
456
|
+
}
|
|
457
|
+
// w1: Writer[Int] = repl.MdocSession$MdocApp32$$anon$43@3bcbf5bd
|
|
458
|
+
|
|
459
|
+
val w2 = new Writer[Int] {
|
|
460
|
+
def isClosed = false
|
|
461
|
+
def write(a: Int) = { collected += a * 10; true }
|
|
462
|
+
def close() = ()
|
|
463
|
+
}
|
|
464
|
+
// w2: Writer[Int] = repl.MdocSession$MdocApp32$$anon$45@3ed3f62
|
|
465
|
+
|
|
466
|
+
val combined = w1 ++ w2
|
|
467
|
+
// combined: Writer[Int] = zio.blocks.streams.io.Writer$ConcatWith@6f75d7cc
|
|
468
|
+
combined.write(5)
|
|
469
|
+
// res33: Boolean = true
|
|
470
|
+
combined.write(20) // first writer rejects, switches to second
|
|
471
|
+
// res34: Boolean = true
|
|
472
|
+
println(collected.toList) // List(5, 200)
|
|
473
|
+
// List(5, 200)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### Transformation
|
|
477
|
+
|
|
478
|
+
`Writer#contramap` — Returns a `Writer` that transforms incoming elements with `g` before passing them to this writer. All other operations (`Writer#isClosed`, `Writer#close`, `Writer#fail`) delegate unchanged:
|
|
479
|
+
|
|
480
|
+
```scala
|
|
481
|
+
abstract class Writer[-Elem] {
|
|
482
|
+
def contramap[Elem2](g: Elem2 => Elem): Writer[Elem2]
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Transform the input type before writing:
|
|
487
|
+
|
|
488
|
+
```scala
|
|
489
|
+
import zio.blocks.streams.io.Writer
|
|
490
|
+
|
|
491
|
+
val stringWriter = new Writer[String] {
|
|
492
|
+
def isClosed = false
|
|
493
|
+
def write(a: String) = { println(s"Writing: $a"); true }
|
|
494
|
+
def close() = ()
|
|
495
|
+
}
|
|
496
|
+
// stringWriter: Writer[String] = repl.MdocSession$MdocApp36$$anon$51@23d463e2
|
|
497
|
+
|
|
498
|
+
val intWriter = stringWriter.contramap[Int](_.toString)
|
|
499
|
+
// intWriter: Writer[Int] = zio.blocks.streams.io.Writer$Contramapped@254fb1b1
|
|
500
|
+
intWriter.write(42) // Prints: Writing: 42
|
|
501
|
+
// Writing: 42
|
|
502
|
+
// res37: Boolean = true
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Closure and Error Handling
|
|
506
|
+
|
|
507
|
+
Writers support both clean closure and error closure, allowing you to signal end-of-stream gracefully or with an error condition:
|
|
508
|
+
|
|
509
|
+
### Clean Closure
|
|
510
|
+
|
|
511
|
+
`Writer#close` — Closes the writer cleanly. After this call, `write()` returns `false` and `Writer#isClosed` returns `true`. Idempotent:
|
|
512
|
+
|
|
513
|
+
```scala
|
|
514
|
+
abstract class Writer[-Elem] {
|
|
515
|
+
def close(): Unit
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Error Closure
|
|
520
|
+
|
|
521
|
+
`Writer#fail` — Closes the writer with an error. After this call, `Writer#isClosed` returns `true`. Subclasses that override this method may cause `write()` to throw `error` on subsequent calls; the default simply delegates to `Writer#close`. Both `Writer#close` and `Writer#fail` are idempotent; only the first call wins:
|
|
522
|
+
|
|
523
|
+
```scala
|
|
524
|
+
abstract class Writer[-Elem] {
|
|
525
|
+
def fail(error: Throwable): Unit
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Close a writer with an error:
|
|
530
|
+
|
|
531
|
+
```scala
|
|
532
|
+
import zio.blocks.streams.io.Writer
|
|
533
|
+
|
|
534
|
+
val w = Writer.single[Int]
|
|
535
|
+
// w: Writer[Int] = zio.blocks.streams.io.Writer$SingleWriter@7e886598
|
|
536
|
+
w.write(42)
|
|
537
|
+
// res39: Boolean = true
|
|
538
|
+
w.fail(new RuntimeException("Error"))
|
|
539
|
+
println(w.isClosed) // true
|
|
540
|
+
// true
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Contravariance
|
|
544
|
+
|
|
545
|
+
`Writer` is **contravariant** in `Elem`, meaning `Writer[-Elem]` can accept narrower types. If you have a `Writer[Number]`, you can use it as a `Writer[Int]` because every `Int` is a `Number`:
|
|
546
|
+
|
|
547
|
+
```scala
|
|
548
|
+
import zio.blocks.streams.io.Writer
|
|
549
|
+
|
|
550
|
+
trait Number
|
|
551
|
+
case class IntNum(value: Int) extends Number
|
|
552
|
+
|
|
553
|
+
val numberWriter = new Writer[Number] {
|
|
554
|
+
def isClosed = false
|
|
555
|
+
def write(a: Number) = { println(s"Number: $a"); true }
|
|
556
|
+
def close() = ()
|
|
557
|
+
}
|
|
558
|
+
// numberWriter: Writer[Number] = repl.MdocSession$MdocApp42$$anon$59@290eac28
|
|
559
|
+
|
|
560
|
+
// numberWriter is also a Writer[IntNum] due to contravariance
|
|
561
|
+
val intNumWriter: Writer[IntNum] = numberWriter
|
|
562
|
+
// intNumWriter: Writer[IntNum] = repl.MdocSession$MdocApp42$$anon$59@290eac28
|
|
563
|
+
intNumWriter.write(IntNum(42))
|
|
564
|
+
// Number: IntNum(42)
|
|
565
|
+
// res43: Boolean = true
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
This is the dual of Reader's covariance: Reader is covariant (`+Elem`) because narrower elements flow out; Writer is contravariant (`-Elem`) because broader element types flow in.
|
|
569
|
+
|
|
570
|
+
## Integration with Readers and Channels
|
|
571
|
+
|
|
572
|
+
While Reader is typically used with pull-based stream operations, Writer is used internally by channel-based implementations and as an I/O adapter. The pairing is natural: a Reader pulls from a source, while a Writer pushes to a sink.
|
|
573
|
+
|
|
574
|
+
For typical stream usage, you'll see Writer indirectly when writing to files, network sockets, or other I/O resources. The `Writer.fromOutputStream` and `Writer.fromWriter` factories adapt standard Java I/O to the Writer interface.
|
|
575
|
+
|
|
576
|
+
## Implementation Notes
|
|
577
|
+
|
|
578
|
+
Understanding `Writer`'s design decisions helps you use it correctly and avoid common pitfalls:
|
|
579
|
+
|
|
580
|
+
### Push vs Pull
|
|
581
|
+
|
|
582
|
+
`Writer` is push-based (producer-driven), contrasting with `Reader` which is pull-based (consumer-driven):
|
|
583
|
+
|
|
584
|
+
| Aspect | Reader | Writer |
|
|
585
|
+
|----------------|----------------------------|-------------------------|
|
|
586
|
+
| **Direction** | Source → Consumer (pull) | Producer → Sink (push) |
|
|
587
|
+
| **Variance** | Covariant (`+Elem`) | Contravariant (`−Elem`) |
|
|
588
|
+
| **Blocking** | `read()` may block | `write()` may block |
|
|
589
|
+
| **Signal end** | Returns sentinel or `null` | `close()` or `fail()` |
|
|
590
|
+
| **Dual** | Sink drains Reader | Producer feeds Writer |
|
|
591
|
+
|
|
592
|
+
### Thread Safety
|
|
593
|
+
|
|
594
|
+
`Writer` is **not thread-safe** by default. It is designed for single-threaded, push-based production. Do not share a `Writer` across threads without external synchronization. If you need concurrent production, wrap the writer in a thread-safe queue or use a concurrent streaming library.
|
|
595
|
+
|
|
596
|
+
### Idempotency
|
|
597
|
+
|
|
598
|
+
Both `close()` and `fail()` are idempotent: only the first call wins. Subsequent calls have no effect. This simplifies error handling in try-finally blocks.
|
|
599
|
+
|
|
600
|
+
## Running the Examples
|
|
601
|
+
|
|
602
|
+
All code from this guide is available as runnable examples in the `streams-examples` module.
|
|
603
|
+
|
|
604
|
+
**1. Clone the repository and navigate to the project:**
|
|
605
|
+
|
|
606
|
+
Run these commands to set up the examples:
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
git clone https://github.com/zio/zio-blocks.git
|
|
610
|
+
cd zio-blocks
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**2. Run individual examples with sbt:**
|
|
614
|
+
|
|
615
|
+
### Basic Writer Construction
|
|
616
|
+
|
|
617
|
+
This example demonstrates the most common writer factories: `Writer.single`, `Writer.limited`, `Writer.closed`, and custom writers via subclassing:
|
|
618
|
+
|
|
619
|
+
```scala title="streams-examples/src/main/scala/writer/WriterBasicConstructionExample.scala"
|
|
620
|
+
/*
|
|
621
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
622
|
+
*
|
|
623
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
624
|
+
* you may not use this file except in compliance with the License.
|
|
625
|
+
* You may obtain a copy of the License at
|
|
626
|
+
*
|
|
627
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
628
|
+
*
|
|
629
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
630
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
631
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
632
|
+
* See the License for the specific language governing permissions and
|
|
633
|
+
* limitations under the License.
|
|
634
|
+
*/
|
|
635
|
+
|
|
636
|
+
package writer
|
|
637
|
+
|
|
638
|
+
import zio.blocks.streams.io.Writer
|
|
639
|
+
import zio.blocks.chunk.Chunk
|
|
640
|
+
import scala.collection.mutable
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Demonstrates the most common Writer factories: single, limited, closed, and
|
|
644
|
+
* custom writers via subclassing. Each writer is fed manually with write() to
|
|
645
|
+
* show how to produce elements.
|
|
646
|
+
*/
|
|
647
|
+
object WriterBasicConstructionExample extends App {
|
|
648
|
+
|
|
649
|
+
println("=== Writer.single ===")
|
|
650
|
+
val singleWriter = Writer.single[Int]
|
|
651
|
+
println(s"Write 42: ${singleWriter.write(42)}")
|
|
652
|
+
println(s"Write 99 (closed): ${singleWriter.write(99)}")
|
|
653
|
+
println(s"isClosed: ${singleWriter.isClosed}")
|
|
654
|
+
|
|
655
|
+
println("\n=== Writer.closed ===")
|
|
656
|
+
val closedWriter = Writer.closed
|
|
657
|
+
println(s"Write to closed: ${closedWriter.write(1)}")
|
|
658
|
+
println(s"isClosed: ${closedWriter.isClosed}")
|
|
659
|
+
|
|
660
|
+
println("\n=== Writer.limited ===")
|
|
661
|
+
val limitedWriter = Writer.limited(Writer.single[String], 2)
|
|
662
|
+
val a = "a"
|
|
663
|
+
val b = "b"
|
|
664
|
+
val c = "c"
|
|
665
|
+
println(s"Write 'a': ${limitedWriter.write(a)}")
|
|
666
|
+
println(s"Write 'b': ${limitedWriter.write(b)}")
|
|
667
|
+
println(s"Write 'c': ${limitedWriter.write(c)}")
|
|
668
|
+
println(s"isClosed: ${limitedWriter.isClosed}")
|
|
669
|
+
|
|
670
|
+
println("\n=== writeAll: bulk write ===")
|
|
671
|
+
val collected = mutable.ArrayBuffer[Int]()
|
|
672
|
+
val collectWriter = new Writer[Int] {
|
|
673
|
+
def isClosed = false
|
|
674
|
+
def write(a: Int) = { collected += a; true }
|
|
675
|
+
def close(): Unit = ()
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
val chunk = Chunk(10, 20, 30)
|
|
679
|
+
val remaining = collectWriter.writeAll(chunk)
|
|
680
|
+
println(s"Collected: ${collected.toList}")
|
|
681
|
+
println(s"Remaining: $remaining")
|
|
682
|
+
|
|
683
|
+
println("\n=== writeable: check capacity ===")
|
|
684
|
+
val capWriter = Writer.single[Int]
|
|
685
|
+
println(s"writeable before: ${capWriter.writeable()}")
|
|
686
|
+
capWriter.write(42)
|
|
687
|
+
println(s"writeable after: ${capWriter.writeable()}")
|
|
688
|
+
|
|
689
|
+
println("\n=== Custom Writer ===")
|
|
690
|
+
val upperWriter = new Writer[String] {
|
|
691
|
+
private val buffer = mutable.ArrayBuffer[String]()
|
|
692
|
+
def isClosed = false
|
|
693
|
+
def write(a: String) = {
|
|
694
|
+
buffer += a.toUpperCase()
|
|
695
|
+
true
|
|
696
|
+
}
|
|
697
|
+
def close(): Unit = println(s"Final buffer: $buffer")
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
upperWriter.write("hello")
|
|
701
|
+
upperWriter.write("world")
|
|
702
|
+
upperWriter.close()
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
Run this example with:
|
|
707
|
+
|
|
708
|
+
```bash
|
|
709
|
+
sbt "streams-examples/runMain writer.WriterBasicConstructionExample"
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Composition and Transformation
|
|
713
|
+
|
|
714
|
+
This example shows writer composition with `Writer#++` (concat), transformation with `Writer#contramap`, and bulk writes with `Writer#writeAll`:
|
|
715
|
+
|
|
716
|
+
```scala title="streams-examples/src/main/scala/writer/WriterCompositionExample.scala"
|
|
717
|
+
/*
|
|
718
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
719
|
+
*
|
|
720
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
721
|
+
* you may not use this file except in compliance with the License.
|
|
722
|
+
* You may obtain a copy of the License at
|
|
723
|
+
*
|
|
724
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
725
|
+
*
|
|
726
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
727
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
728
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
729
|
+
* See the License for the specific language governing permissions and
|
|
730
|
+
* limitations under the License.
|
|
731
|
+
*/
|
|
732
|
+
|
|
733
|
+
package writer
|
|
734
|
+
|
|
735
|
+
import zio.blocks.streams.io.Writer
|
|
736
|
+
import zio.blocks.chunk.Chunk
|
|
737
|
+
import scala.collection.mutable
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Demonstrates writer composition with ++ (concat), transformation with
|
|
741
|
+
* contramap, and error handling via fail(). Shows how multiple writers can be
|
|
742
|
+
* chained and how transformations are applied before writing.
|
|
743
|
+
*/
|
|
744
|
+
object WriterCompositionExample extends App {
|
|
745
|
+
|
|
746
|
+
println("=== concat: ++ operator ===")
|
|
747
|
+
val results = mutable.ArrayBuffer[Int]()
|
|
748
|
+
|
|
749
|
+
val w1 = new Writer[Int] {
|
|
750
|
+
def isClosed = false
|
|
751
|
+
def write(a: Int) = {
|
|
752
|
+
results += a * 10
|
|
753
|
+
a < 50 // reject values >= 50
|
|
754
|
+
}
|
|
755
|
+
def close(): Unit = ()
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
val w2 = new Writer[Int] {
|
|
759
|
+
def isClosed = false
|
|
760
|
+
def write(a: Int) = {
|
|
761
|
+
results += a * 100
|
|
762
|
+
true
|
|
763
|
+
}
|
|
764
|
+
def close(): Unit = ()
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
val combined = w1 ++ w2
|
|
768
|
+
|
|
769
|
+
combined.write(10) // accepted by w1 (10 < 50)
|
|
770
|
+
combined.write(60) // rejected by w1, switches to w2
|
|
771
|
+
println(s"Results: ${results.toList}")
|
|
772
|
+
|
|
773
|
+
println("\n=== contramap: transform elements ===")
|
|
774
|
+
val stringResults = mutable.ArrayBuffer[String]()
|
|
775
|
+
val stringWriter = new Writer[String] {
|
|
776
|
+
def isClosed = false
|
|
777
|
+
def write(a: String) = {
|
|
778
|
+
stringResults += a
|
|
779
|
+
true
|
|
780
|
+
}
|
|
781
|
+
def close(): Unit = ()
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
val intWriter = stringWriter.contramap[Int](_.toString)
|
|
785
|
+
intWriter.write(42)
|
|
786
|
+
intWriter.write(99)
|
|
787
|
+
println(s"String results: ${stringResults.toList}")
|
|
788
|
+
|
|
789
|
+
println("\n=== Multiple contramap: chained transformations ===")
|
|
790
|
+
val doubleResults = mutable.ArrayBuffer[String]()
|
|
791
|
+
val doubleStringWriter = new Writer[String] {
|
|
792
|
+
def isClosed = false
|
|
793
|
+
def write(a: String) = {
|
|
794
|
+
doubleResults += a
|
|
795
|
+
true
|
|
796
|
+
}
|
|
797
|
+
def close(): Unit = ()
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
val intDoubleWriter = doubleStringWriter
|
|
801
|
+
.contramap[Double](d => s"${d * 2}")
|
|
802
|
+
.contramap[Int](i => i.toDouble)
|
|
803
|
+
|
|
804
|
+
intDoubleWriter.write(5) // 5 -> 5.0 -> "10.0"
|
|
805
|
+
intDoubleWriter.write(10) // 10 -> 10.0 -> "20.0"
|
|
806
|
+
println(s"Double results: ${doubleResults.toList}")
|
|
807
|
+
|
|
808
|
+
println("\n=== fail: error closure ===")
|
|
809
|
+
val failWriter = new Writer[Int] {
|
|
810
|
+
private var closed = false
|
|
811
|
+
def isClosed = closed
|
|
812
|
+
def write(a: Int) =
|
|
813
|
+
if (closed) false else { println(s"Write: $a"); true }
|
|
814
|
+
def close(): Unit = closed = true
|
|
815
|
+
override def fail(error: Throwable): Unit = {
|
|
816
|
+
closed = true
|
|
817
|
+
println(s"Failed with: ${error.getMessage}")
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
failWriter.write(1)
|
|
822
|
+
failWriter.fail(new RuntimeException("Oops"))
|
|
823
|
+
println(s"isClosed after fail: ${failWriter.isClosed}")
|
|
824
|
+
|
|
825
|
+
println("\n=== writeAll: bulk operations ===")
|
|
826
|
+
val bulkResults = mutable.ArrayBuffer[Int]()
|
|
827
|
+
val bulkWriter = Writer.limited(
|
|
828
|
+
new Writer[Int] {
|
|
829
|
+
def isClosed = false
|
|
830
|
+
def write(a: Int) = { bulkResults += a; true }
|
|
831
|
+
def close(): Unit = ()
|
|
832
|
+
},
|
|
833
|
+
2
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
val chunk = Chunk(1, 2, 3, 4)
|
|
837
|
+
val unwritten = bulkWriter.writeAll(chunk)
|
|
838
|
+
println(s"Bulk results: ${bulkResults.toList}")
|
|
839
|
+
println(s"Unwritten: $unwritten")
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
Run this example with:
|
|
844
|
+
|
|
845
|
+
```bash
|
|
846
|
+
sbt "streams-examples/runMain writer.WriterCompositionExample"
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
### I/O Adapters
|
|
850
|
+
|
|
851
|
+
This example demonstrates I/O integration with `Writer.fromOutputStream` and `Writer.fromWriter` for streaming to files or character streams:
|
|
852
|
+
|
|
853
|
+
```scala title="streams-examples/src/main/scala/writer/WriterIOAdapterExample.scala"
|
|
854
|
+
/*
|
|
855
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
856
|
+
*
|
|
857
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
858
|
+
* you may not use this file except in compliance with the License.
|
|
859
|
+
* You may obtain a copy of the License at
|
|
860
|
+
*
|
|
861
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
862
|
+
*
|
|
863
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
864
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
865
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
866
|
+
* See the License for the specific language governing permissions and
|
|
867
|
+
* limitations under the License.
|
|
868
|
+
*/
|
|
869
|
+
|
|
870
|
+
package writer
|
|
871
|
+
|
|
872
|
+
import zio.blocks.streams.io.Writer
|
|
873
|
+
import java.io.{ByteArrayOutputStream, StringWriter}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* Demonstrates I/O integration with Writer via fromOutputStream and fromWriter.
|
|
877
|
+
* Shows how to write bytes to streams and characters to writers using the
|
|
878
|
+
* Writer interface.
|
|
879
|
+
*/
|
|
880
|
+
object WriterIOAdapterExample extends App {
|
|
881
|
+
|
|
882
|
+
println("=== Writer.fromOutputStream ===")
|
|
883
|
+
val byteStream = new ByteArrayOutputStream()
|
|
884
|
+
val byteWriter = Writer.fromOutputStream(byteStream)
|
|
885
|
+
|
|
886
|
+
byteWriter.write(72.toByte) // 'H'
|
|
887
|
+
byteWriter.write(105.toByte) // 'i'
|
|
888
|
+
byteWriter.write(33.toByte) // '!'
|
|
889
|
+
byteWriter.close()
|
|
890
|
+
|
|
891
|
+
println(s"Output: ${byteStream.toString("UTF-8")}")
|
|
892
|
+
|
|
893
|
+
println("\n=== writeBytes: bulk byte write ===")
|
|
894
|
+
val byteStream2 = new ByteArrayOutputStream()
|
|
895
|
+
val byteWriter2 = Writer.fromOutputStream(byteStream2)
|
|
896
|
+
|
|
897
|
+
val message = "Hello".getBytes("UTF-8")
|
|
898
|
+
val bytesWritten = byteWriter2.writeBytes(message, 0, message.length)
|
|
899
|
+
byteWriter2.close()
|
|
900
|
+
|
|
901
|
+
println(s"Bytes written: $bytesWritten")
|
|
902
|
+
println(s"Output: ${byteStream2.toString("UTF-8")}")
|
|
903
|
+
|
|
904
|
+
println("\n=== Writer.fromWriter ===")
|
|
905
|
+
val charStream = new StringWriter()
|
|
906
|
+
val charWriter = Writer.fromWriter(charStream)
|
|
907
|
+
|
|
908
|
+
charWriter.write('H')
|
|
909
|
+
charWriter.write('e')
|
|
910
|
+
charWriter.write('l')
|
|
911
|
+
charWriter.write('l')
|
|
912
|
+
charWriter.write('o')
|
|
913
|
+
charWriter.close()
|
|
914
|
+
|
|
915
|
+
println(s"Output: ${charStream.toString}")
|
|
916
|
+
|
|
917
|
+
println("\n=== writeChar: individual character writes ===")
|
|
918
|
+
val charStream2 = new StringWriter()
|
|
919
|
+
val charWriter2 = Writer.fromWriter(charStream2)
|
|
920
|
+
|
|
921
|
+
val greeting = "Hi!"
|
|
922
|
+
for (c <- greeting) {
|
|
923
|
+
val result = charWriter2.writeChar(c)
|
|
924
|
+
println(s"Write '$c': $result")
|
|
925
|
+
}
|
|
926
|
+
charWriter2.close()
|
|
927
|
+
|
|
928
|
+
println(s"Output: ${charStream2.toString}")
|
|
929
|
+
|
|
930
|
+
println("\n=== Specialized numeric writes ===")
|
|
931
|
+
val charStream3 = new StringWriter()
|
|
932
|
+
val charWriter3 = Writer.fromWriter(charStream3)
|
|
933
|
+
|
|
934
|
+
// Note: These specialized methods require the writer to be typed to accept them
|
|
935
|
+
// For a demo, we'll just show the interface exists
|
|
936
|
+
println("Specialized write methods available:")
|
|
937
|
+
println(" - writeInt(value: Int)")
|
|
938
|
+
println(" - writeLong(value: Long)")
|
|
939
|
+
println(" - writeFloat(value: Float)")
|
|
940
|
+
println(" - writeDouble(value: Double)")
|
|
941
|
+
println(" - writeBoolean(value: Boolean)")
|
|
942
|
+
println(" - writeShort(value: Short)")
|
|
943
|
+
|
|
944
|
+
charWriter3.close()
|
|
945
|
+
|
|
946
|
+
println("\n=== Error handling in I/O ===")
|
|
947
|
+
val closedStream = new ByteArrayOutputStream()
|
|
948
|
+
closedStream.close()
|
|
949
|
+
val failingWriter = Writer.fromOutputStream(closedStream)
|
|
950
|
+
|
|
951
|
+
val writeResult = failingWriter.write(65.toByte) // 'A'
|
|
952
|
+
println(s"Write to closed stream: $writeResult")
|
|
953
|
+
println(s"Writer is closed: ${failingWriter.isClosed}")
|
|
954
|
+
}
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
Run this example with:
|
|
958
|
+
|
|
959
|
+
```bash
|
|
960
|
+
sbt "streams-examples/runMain writer.WriterIOAdapterExample"
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
### Bounded Implementation
|
|
964
|
+
|
|
965
|
+
This example shows how to implement a bounded Writer that wraps a fixed-capacity container and auto-closes when full. It demonstrates the protocol: `write()` returns `false` only on closure (not buffer fullness), and `writeable()` reflects closure state:
|
|
966
|
+
|
|
967
|
+
```scala title="streams-examples/src/main/scala/writer/WriterBoundedImplementationExample.scala"
|
|
968
|
+
/*
|
|
969
|
+
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
|
|
970
|
+
*
|
|
971
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
972
|
+
* you may not use this file except in compliance with the License.
|
|
973
|
+
* You may obtain a copy of the License at
|
|
974
|
+
*
|
|
975
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
976
|
+
*
|
|
977
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
978
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
979
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
980
|
+
* See the License for the specific language governing permissions and
|
|
981
|
+
* limitations under the License.
|
|
982
|
+
*/
|
|
983
|
+
|
|
984
|
+
package writer
|
|
985
|
+
|
|
986
|
+
import zio.blocks.streams.io.Writer
|
|
987
|
+
import scala.collection.mutable
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Demonstrates implementing a bounded Writer that auto-closes when capacity is
|
|
991
|
+
* reached. Shows how write() returns false only on closure, not on buffer
|
|
992
|
+
* fullness, and how writeable() reflects the closure state.
|
|
993
|
+
*/
|
|
994
|
+
object WriterBoundedImplementationExample extends App {
|
|
995
|
+
|
|
996
|
+
class BoundedWriter[A](maxCapacity: Int) extends Writer[A] {
|
|
997
|
+
private val buffer = mutable.Buffer[A]()
|
|
998
|
+
private var closed = false
|
|
999
|
+
|
|
1000
|
+
def isClosed: Boolean = closed
|
|
1001
|
+
|
|
1002
|
+
def write(a: A): Boolean =
|
|
1003
|
+
if (closed) false
|
|
1004
|
+
else if (buffer.size < maxCapacity) {
|
|
1005
|
+
buffer += a
|
|
1006
|
+
true
|
|
1007
|
+
} else {
|
|
1008
|
+
// Buffer full: auto-close and reject
|
|
1009
|
+
closed = true
|
|
1010
|
+
false
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
def close(): Unit = closed = true
|
|
1014
|
+
|
|
1015
|
+
override def fail(error: Throwable): Unit = close()
|
|
1016
|
+
|
|
1017
|
+
def contents: mutable.Buffer[A] = buffer
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
println("=== Bounded Writer with auto-close ===")
|
|
1021
|
+
val bounded = new BoundedWriter[Int](3)
|
|
1022
|
+
|
|
1023
|
+
println(s"Write 10: ${bounded.write(10)}")
|
|
1024
|
+
println(s"Write 20: ${bounded.write(20)}")
|
|
1025
|
+
println(s"Write 30: ${bounded.write(30)}")
|
|
1026
|
+
println(s"Write 40 (buffer full, auto-closes): ${bounded.write(40)}")
|
|
1027
|
+
println(s"Write 50 (closed): ${bounded.write(50)}")
|
|
1028
|
+
|
|
1029
|
+
println(s"\nBuffer contents: ${bounded.contents}")
|
|
1030
|
+
println(s"Writer closed: ${bounded.isClosed}")
|
|
1031
|
+
println(s"Writeable: ${bounded.writeable()}")
|
|
1032
|
+
|
|
1033
|
+
println("\n=== Behavior summary ===")
|
|
1034
|
+
println("• write() returns true while space exists")
|
|
1035
|
+
println("• When buffer fills, write() auto-closes and returns false")
|
|
1036
|
+
println("• All subsequent write() calls return false (closure is permanent)")
|
|
1037
|
+
println("• writeable() reflects closure state, not buffer capacity")
|
|
1038
|
+
}
|
|
1039
|
+
```
|
|
1040
|
+
|
|
1041
|
+
Run this example with:
|
|
1042
|
+
|
|
1043
|
+
```bash
|
|
1044
|
+
sbt "streams-examples/runMain writer.WriterBoundedImplementationExample"
|
|
1045
|
+
```
|