Read a file
Read a whole file or its lines, hand the failure to the caller with the question mark operator, and turn an IoError into your own error type.
Reading a file needs std/fs, and the import is the point: a file that touches files says so on its first line. Every
function of File that can fail answers a Result, so the failure travels up with ? and cannot be forgotten.
Steps#
Import what you use.
std/fsis deliberately not in the prelude, so the import is a statement about what the file can reach.use File, IoError from "std/fs"Read the whole file with
File.readText. It answersResult<String, IoError>. AStringis always valid UTF-8, so bytes that are not are anIoErrorrather than a replacement character.const text = File.readText(path)?Declare the failure in your signature. A function that uses
?has to answer aResult(or anOption). The shortest form hands theIoErrorstraight on:fn readConfig(path: String): Result<String, IoError>Convert to your own error type where you have one.
?appliesFromon the way out, and a case that wraps exactly one value of a unique type gets thatFromgenerated. So nothing has to be written for the conversion.type AppError { case Io(cause: IoError) case Empty(path: String) }Read line by line for a file that does not fit in memory.
File.linesanswersResult<Iterate<String>, IoError>and the file is never in memory as a whole. The pipeline is lazy, so atakestops reading.for line in File.lines(path)? { print line }Use
File.openandusingonly when you need the handle. An open file has the identity of an operating-system handle, so it is ashared typewithClose.usingbinds it to a name that cannot leave the block, and the file is released - and closed - where the block ends. Nothing callsclose()by hand. Most code does not need a handle.fn firstText(path: String): Result<String, IoError> { using file = File.open(path)? file.readAll() }
Pitfalls#
File.existsis not a precondition. It answers aBooland cannot fail, but the file can disappear between the two calls. Read and handle the failure instead of asking first.?needs aResultaround it. Inside a function that answers a plain value,?is a compile error. Either change the signature or match on theResult.text.lines()andFile.lines(path)are different. The first splits aStringyou already have; the second streams a file.- A
Stringhas nolength(). Usetext.byteLength()for bytes, which is O(1), ortext.chars().count()for characters. - Do not reach for a
tryblock. There is none. The whole error path is the?and the result type.
Full example#
use File, IoError from "std/fs"
/** What can go wrong while the configuration is read. `From<IoError>` is generated for the `Io` case. */
type ConfigError with Show, Error {
case Io(cause: IoError)
case Empty(path: String)
fn show(): String {
match self {
.Io(cause) => "{cause}"
.Empty(path) => "{path} is empty"
}
}
}
/** The first line of the file, or a `ConfigError` that says why there is none. */
fn firstLine(path: String): Result<String, ConfigError> {
const text = File.readText(path)?
const lines = text.lines()
match lines.first() {
Some(line) => line
None => Fail ConfigError.Empty(path)
}
}
fn describe(path: String): String {
match firstLine(path) {
Ok(line) => "first line: {line}"
Fail(problem) => "no first line: {problem}"
}
}
print describe("project.trb")Related#
- Result - the rules of
Ok,Failand?. - std/core - where
ResultandErrorare declared. - Cases and match - the generated
Fromof a single-value case. - Write a configuration file - the other half of reading configuration.