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.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
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.