Streams in a Nutshell
A Stream[E, A] describes a sequence of A values that arrives piece by piece, and that may fail with an error of type E. You build a pipeline by transforming the stream, then run it to get a result. Because a stream pulls one element at a time, it processes files, sockets and query results that do not fit in memory.
The Problem It Solves
Reading a large file into a List works until the file is larger than the heap. Iterators avoid that, but they have no way to report a typed error, no way to close the file when processing stops early, and no clear point where the work happens.
A stream fixes all three. Its error type is part of its type, it can own the resource it reads from, and nothing happens until you run it.
Describe, Then Run
We describe a stream of orders and a pipeline over it. These lines do no work yet; they only describe what should happen:
import zio.blocks.streams._
final case class Order(id: Int, amountCents: Long, paid: Boolean)
val orders: Stream[Nothing, Order] = Stream(
Order(1, 2500L, paid = true),
Order(2, 1200L, paid = false),
Order(3, 4800L, paid = true),
Order(4, 900L, paid = true)
)
val paidAmounts: Stream[Nothing, Long] = orders.filter(_.paid).map(_.amountCents)
The type Stream[Nothing, Order] says that this stream cannot fail. Running it does the work and returns a plain Either, with the error on the left and the result on the right:
paidAmounts.runCollect
// res0: Either[Nothing, Chunk[Long]] = Right(IndexedSeq(2500L, 4800L, 900L))
paidAmounts.runFold(0L)((total, cents) => total + cents)
// res1: Either[Nothing, Long] = Right(8200L)
The same paidAmounts value can run again, and each run starts from the beginning.
Errors Are Part of the Type
A step that can fail turns a Stream[Nothing, A] into a stream with a real error type. Here each text line becomes an order, or fails with a BadLine error:
final case class BadLine(line: String)
def parse(line: String): Stream[BadLine, Order] =
line.split(",") match {
case Array(id, cents, paid) if id.toIntOption.isDefined && cents.toLongOption.isDefined =>
Stream.succeed(Order(id.toInt, cents.toLong, paid == "paid"))
case _ =>
Stream.fail(BadLine(line))
}
val parsed: Stream[BadLine, Order] =
Stream("1,2500,paid", "2,oops,unpaid", "3,4800,paid").flatMap(parse)
Running the stream stops at the first error and returns it as a value:
parsed.runCollect
// res2: Either[BadLine, Chunk[Order]] = Left(BadLine("2,oops,unpaid"))
Stream#catchAll replaces a failure with another stream. Applied to the parsing step of each line, it turns a bad line into an empty stream, so the pipeline skips that line and keeps going, and the error type becomes Nothing again:
Stream("1,2500,paid", "2,oops,unpaid", "3,4800,paid")
.flatMap(line => parse(line).catchAll(_ => Stream.empty))
.map(_.id)
.runCollect
// res3: Either[Nothing, Chunk[Int]] = Right(IndexedSeq(1, 3))
Applied to the whole parsed stream instead, catchAll would take over at the first error and end the run there, without order 3.
What Else Streams Give You
The block is built for real input and output:
Stream.fromAcquireReleaseopens a file or a connection when the stream starts and closes it when the stream ends, even if it fails or stops early.- A
Sinkdescribes how to consume a stream, such as counting, folding or writing to a file, andStream#runconnects the two. - Elements of type
Int,Long,Doubleand the other primitives stay unboxed as they flow through a pipeline. - The run methods on this page block the calling thread and exist on the JVM. On Scala.js, or whenever a thread must not block, use the
Asyncvariants such asStream#runCollectAsync.
Learn More
The Your First Streaming Pipeline tutorial builds a complete pipeline over a log file, and the Streams reference documents every operator. The next page shows how to use ZIO Blocks with ZIO, cats-effect or plain Scala.