DocsStandard library

Readers and writers.

Parse and emit byte streams with scoped buffering.

A byte source hands out chunks, and a reader puts an advancing buffer in front of it. A sink accepts chunks, and a writer gathers your output before passing it along. The nice part is that the same parser can work on memory, a file, or a socket.

API reference: Reader · Writer · Bytes

Reader.

We’ll begin with an in-memory example:

import Bytes
import Reader

prefix contents = Reader.withBytes contents { reader -> Reader.readUpTo reader 4 }

main() = print (Bytes.toString (prefix (Bytes.fromString "Fango")))

This prints Just "Fang". withBytes creates a private cursor over the bytes. Your callback can return parsed data, but not the reader itself or a function that holds on to its local permission. With no other effects involved, the whole operation is pure.

To read from a source instead, use Reader.over source action. Keep any file- or socket-backed reading inside the scope that owns the handle.

Choose a read operation.

readUpTo reader count waits until some input is available and returns what it has. A short, nonempty result doesn’t mean the input has ended. readExactly waits for the full count instead, and returns Nothing, consuming nothing, if there isn’t enough input left.

readUntil reader delimiter limit bounds how much input can be buffered for the run and the delimiter together. It returns Found without the delimiter, Ended with an unterminated remainder, or Overflowed without consuming anything. When the input comes from an untrusted peer, choose a limit that makes sense for your protocol.

readLine uses LF as the delimiter. The byte version leaves a preceding CR in the bytes it returns, while the text reader treats CRLF as a line terminator and strips it.

Source and Sink.

A source’s pull() returns Nothing at the end of input, and a Just with an empty chunk isn’t the end. A sink’s write finishes writing the chunk you gave it before it returns. These adapters never close handles. Their effect parameter is a row, so a memory source can be pure while a file source carries File.FileSystem and failure.

Frame a smaller reader.

A bounded reader keeps a parser from reading into the next frame. This example reads two bytes through a three-byte allowance, then checks where the parent stopped:

import Bytes
import Reader

main() =
    result = Reader.withBytes (Bytes.fromString "abcdef") { parent ->
        prefix = Reader.limited parent 3 { frame -> Reader.readUpTo frame 2 }
        rest = Reader.readUpTo parent 4
        (Bytes.toString prefix, Bytes.toString rest)
    }
    print result
(Just "ab", Just "cdef")

limited moves the parent forward by the bytes that were actually read, not by the whole allowance. If your protocol requires consuming the entire frame, discard the unused bytes yourself before parsing the next one.

The reader’s buffered, refill, and skip fields give parsers finer control over lookahead. Stick with the higher-level reads until you really need to manage those transitions yourself.

Writer.

import Bytes
import Writer

main() =
    (_, bytes) = Writer.collecting { output ->
        Writer.writeString output "hello"
        Writer.writeString output " world"
    }
    print (Bytes.toString bytes)

This prints Just "hello world". collecting returns both the callback’s value and the bytes it collected. A writer over a sink, Writer.over sink size action, flushes whenever its buffer window fills and again on normal completion. A language abort doesn’t do a final flush. A returned Err value still counts as normal completion.

writer.flush() pushes pending bytes out explicitly, and Writer.copy moves the rest of a reader into a writer.

Lifetimes.

Scoped buffered objects can’t escape their callbacks. External handles have a separate runtime lifetime: once a file is closed, its adapters stop working, even if you kept hold of one. Create each task’s buffered objects inside that task, and finish consuming streams before the resource’s scope ends.