I/O Operations in Hare
Learn console output, reading standard input, and file reading and writing in Hare, with tagged-union error handling and Docker-ready examples
Input and output are where a systems language earns its keep. Hare handles I/O through a small set of standard library modules — fmt for formatted text, io for the low-level handle interface, os for process streams and files, and bufio for buffered line-oriented reading. Everything flows through the io::handle abstraction, so the same functions that write to the console also write to a file.
As an imperative systems language, Hare gives you direct, explicit control over I/O with no hidden buffering or allocation. What makes Hare distinctive is that errors cannot be silently ignored. Every operation that can fail returns a tagged union that includes an error type, and the compiler forces you to acknowledge it — either by asserting success with the ! operator or by handling the error with match. This tutorial covers console output, reading from standard input, and reading and writing files, all while respecting Hare’s error-handling discipline.
You have already seen fmt::println in Hello World. Here we go further: formatted output with placeholders and base conversions, buffered input, and the os/io file APIs.
Console Output
Hare’s fmt module provides a family of output functions. The f-prefixed variants (fprint, fprintln) write to a specific handle; the printf/printfln variants interpret a format string with {} placeholders.
Create a file named io_operations.ha:
| |
Note that os::stderr is passed as an ordinary argument. It has type io::file, which satisfies the io::handle interface — the same interface a file handle satisfies, as you will see below.
Reading Input from Standard Input
To read from the terminal (or piped input), wrap os::stdin in a buffered scanner from the bufio module. The scanner reads efficiently and hands you one line at a time with the trailing newline stripped. bufio::scan_line returns a tagged union of (const str | io::EOF | io::error), forcing you to handle end-of-input and read errors explicitly.
Create a file named read_input.ha:
| |
The match expression has three arms: io::EOF (no input), io::error (a genuine read failure, reported with fmt::fatalf, which prints to stderr and exits), and the success case, which yields the line as the value of the match. Because the error and EOF arms terminate, the compiler knows name always ends up being a str.
Writing to a File
os::create opens a new file for writing, truncating any existing file, and returns an io::file. Because that handle satisfies io::handle, the very same fmt::fprintln and fmt::fprintfln functions write to it. The defer statement guarantees the file is closed when main returns, even on an early exit.
Create a file named file_write.ha:
| |
This shows two levels of abstraction: the convenient fmt functions for formatted text, and io::writeall for writing an exact byte slice — useful when you already have binary data or a prepared buffer.
Reading from a File
Reading a file combines the pieces above: open a handle with os::open, wrap it in a scanner, and loop until io::EOF. This example is self-contained — it first writes a small file, then reads it back line by line — so it produces the same output every time.
Create a file named file_read.ha:
| |
The for (true) loop runs until the io::EOF arm executes break. Notice that the same bufio::newscanner / bufio::scan_line pattern reads a file here just as it read the terminal in the previous example — a handle is a handle.
Running with Docker
Alpine Linux’s edge repository packages Hare, so no local install is required. Each command mounts the current directory and installs the compiler before running.
| |
Expected Output
Running io_operations.ha (the final line is written to stderr):
Formatted output in Hare
ABC
Hare first appeared in 2022
echo echo !
255 in hex is ff
255 in octal is 377
255 in binary is 11111111
This message goes to stderr
Running read_input.ha with Ada piped in (the prompt has no newline, so the reply continues on the same line):
Enter your name: Hello, Ada!
Running file_write.ha:
Wrote output.txt (30 bytes on the final write)
The resulting output.txt contains:
Line 1: written with fprintln
Line 2: 6 times 7 is 42
Line 3: written with writeall
Running file_read.ha:
1: apple
2: banana
3: cherry
Key Concepts
- One handle interface —
os::stdout,os::stderr,os::stdin, and files all satisfyio::handle, sofmt::fprintln,io::writeall, and thebufioscanner work uniformly across the console and files. - Errors are unavoidable — I/O functions return tagged unions containing an error type. Use
!to assert success (aborting on failure) ormatchto handle each case explicitly. matchonio::EOF— Reading loops branch on(const str | io::EOF | io::error); theio::EOFarm is how you detect the end of input.- Buffered reading with
bufio—bufio::newscannerwraps a handle for efficient line-by-line reads; pair it withdefer bufio::finish(&scan)to free the buffer. - Format placeholders —
{}substitutes arguments in order,{0}references by index (and can repeat), and modifiers like{:x},{:o}, and{:b}convert integers to hexadecimal, octal, and binary. deferfor cleanup —defer io::close(file)!runs when the function returns, keeping resource release next to acquisition and correct even on early exits.- Two write levels —
fmt::fprintln/fmt::fprintflnfor formatted text,io::writeallwithstrings::toutf8for exact byte control.
Comments
Loading comments...
Leave a Comment