Streams and for!
Streams let a pipeline perform effects as its items are requested. This file starts with a small pipeline that prints messages, then shows how to create a custom stream for reading lines from a file with basic-cli.
Using a stream
Calling .stream() on an iterator lets us use .map!() for an effectful transformation. Here, preparing each value prints a message, and for! consumes the results:
show_pipeline! = || { values = [1.U64, 2, 3].iter().stream().map!( |n| { Stdout.line!("Preparing ${n.to_str()}")? Ok(n * 10) }, ) for! result in values.take_first(2) { value = result? Stdout.line!("Got ${value.to_str()}")? } Ok({}) }
This prints:
Preparing 1 Got 10 Preparing 2 Got 20
Preparation and consumption alternate because the pipeline is lazy. .take_first(2) stops it before the third value is prepared. The Try values and ? handle possible errors when writing to standard output.
An ordinary for loop consumes a pure iterator, although its body may still perform effects. Streams are useful when producing or transforming the items also needs effects, as the .map!() callback does here.
Creating a stream from file reads
The basic-cli platform provides read_line!(), which reads one line at a time. The lines function below adapts those individual reads into a custom stream (the reader also has a ready-made lines() method, this example builds the stream by hand to show how custom streams work). This lets callers use the same pipeline operations and for! syntax as in the first example:
lines = |reader| Stream.custom( Reading(reader), Unknown, |state| match state { Finished => Err(NoMore) Reading(current) => match current.read_line!() { Ok(line) => Ok((Ok(line), Reading(current))) Err(EndOfInput) => Err(NoMore) Err(err) => Ok((Err(err), Finished)) } }, ) show_file! = |path| { reader = File.open_reader!(path)? for! result in lines(reader).take_first(2) { line = result? Stdout.line!(line)? } Ok({}) }
A custom stream supplies its initial state, a size hint (Unknown here), and a callback that produces the next item and state. In this example:
Reading(reader)keeps the buffered reader available for the next pull.Err(EndOfInput)signals the end of the file, represented byErr(NoMore). A blank line isOk(""), so it is still an item.- A read error is yielded as an item containing
Err(err), followed by theFinishedstate. This lets the caller report the error rather than silently treating it as the end of the file.
The loop uses ? to propagate read and UTF-8 errors. Since read_line!() removes the newline, Stdout.line!() adds it back when printing. The reader closes automatically when its last reference is dropped.
The sample file contains three lines:
Hello from a file! Streams read on demand. This third line is not requested.
Output
Run both examples from the directory that has main.roc and input.txt in it:
$ roc main.roc -- input.txt Preparing 1 Got 10 Preparing 2 Got 20 Hello from a file! Streams read on demand.
Only two lines are requested from the reader. basic-cli may buffer bytes ahead internally, but the app does not load the whole file into a string or a list of lines.
Validated with Roc nightly-2026-10-29-a0a9fc2.