FileHandle
type ReadError
Why a read failed.
ReadFailed means the source refused the read. InvalidUtf8 means bytes were read but are not valid UTF-8, and carries the byte offset of the first malformed sequence, relative to that one operation. A failed read consumes the bytes it attempted to read and never substitutes U+FFFD: use readBytes to recover the payload verbatim.
Variants
ReadFailedInvalidUtf8(Integer)
trait Readable
Readable: a source that yields text.
Named so that anything can be one, not just a file: the vocabulary FileHandle<CanRead, W> already carried was an abstraction without a name, so nothing else could implement it and IO did not go through it (kexhq/kex#139). IO.in is a Readable; so is any handle opened for reading.
foul firstLine(source: Readable) -> String do
source.getLine.or("(empty)")
end
firstLine(IO.in)
firstLine(FS.File.open("notes.txt", Read).try)
Implemented by FileHandle<CanRead, W>.
Required methods
An implementor writes these.
getLine
getLine : Result<String?, ReadError>
Reads the next line, without its newline.
Returns the next line, Ok(None) at end of
get
get : Result<String?, ReadError>
Reads a single character, as a one-character String.
Reads one complete Unicode scalar, not one byte.
Returns the next character, Ok(None) at end
readLine
readLine : Result<String?, ReadError>
Reads the next line, without its newline. The same as getLine.
Returns the next line, Ok(None) at end of
read
read : Result<String, ReadError>
Reads everything remaining, as one String.
Draining an exhausted source answers Ok("").
Returns the remaining contents, or the failure
readBytes
readBytes : Result<Binary, ReadError>
Reads everything remaining as raw bytes, without decoding it as text.
The byte counterpart of read: it never validates UTF-8, so it recovers the payload of a source that is not text, or one read has just rejected. Draining an exhausted source answers Ok(Binary.fromBytes([])).
Returns the remaining bytes, or the failure
trait Writable
Writable: a sink that accepts text.
The payoff of naming it is that a sink becomes a VALUE a library can accept, rather than a global switch it can only sit underneath: output from one library can go to a buffer while another's goes to the terminal (kexhq/kex#139).
foul report(out: Writable, lines: [String]) -> Void do
lines.each { |line| out.printLine(line) }
end
report(IO.out, results)
report(IO.error, warnings)
report(FS.File.open("report.txt", Write).try, results)
Two deliberate choices, both settled in kexhq/kex#139:
- The argument is
Showable, notString.IO.printLinealways took aShowablewhile the handle methods took aString; the wider one is right, and it is what makesIO.printLine(x)andIO.out.printLine(x)the same call. - The result isVoid, notBool. A boolean nobody checks is not an error channel, andResult<Void, IOError>on every print is miserable to use. Erlang's answer is the one taken here: the call saysok, and failure belongs to the device rather than to the call site.
Implemented by FileHandle<R, CanWrite>.
Required methods
An implementor writes these.
printLine
printLine(content: Showable) -> Void
Writes content followed by a newline.
Parameters
content- the value to write
print
print(content: Showable) -> Void
Writes content with no trailing newline.
Parameters
content- the value to write
writeLine
writeLine(content: Showable) -> Void
Writes content followed by a newline. The same as printLine.
Parameters
content- the value to write
write
write(content: Showable) -> Void
Writes content with no trailing newline. The same as print.
Parameters
content- the value to write
writeBytes
writeBytes(content: Binary) -> Void
Writes content as raw bytes, with no trailing newline.
The byte counterpart of write: the payload goes to the sink exactly as given. It never renders the value, so a Binary reaches the sink as its bytes rather than as the #Binary<N bytes> that Showable would print.
Parameters
content- the bytes to write
type FileHandle<R, W>
An open file, obtained from FS.File.open.
The two type parameters record what the handle is allowed to do: R is CanRead or CannotRead, W is CanWrite or CannotWrite. FS.File.open picks them from the mode you pass, so calling write on a handle opened Read is a compile error rather than a run-time failure.
using FS
main do
match FS.File.open("notes.txt", Read) do
Ok(handle) => do
IO.printLine(handle.read.or(""))
handle.close
end
Error(e) => IO.printError("cannot open: ${e}")
end
end
Reach for a handle when you want to walk a large file a line at a time, or make many small writes. When a file fits comfortably in memory, FS.File.read and FS.File.write are shorter and need no closing. To be rid of the closing entirely, pass FS.File.open a block.
The handle methods are foul: obtaining a handle is not an effect, but reading or writing through one is, so a function that does so is foul no matter where the handle came from. Injection makes a thing substitutable, not pure.
Implements Readable, Writable.
Methods
close
close : Void
Closes the handle, flushing anything still buffered.
Close every handle you open. A written file is not guaranteed to be complete on disk until its handle is closed. Passing FS.File.open a block closes the handle for you.
Examples
match FS.File.open("out.txt", Write) do
Ok(handle) => do
handle.printLine("hello")
handle.close
end
Error(_) => IO.printError("cannot open out.txt")
end
seek
seek(offset: Integer) -> Result<Void, ReadError>
Moves the handle's cursor to an absolute byte offset from the start of the file. Read and write share one cursor, so this repositions both — a readLine right after seek(0) starts over from the top, and a write right after does too, overwriting from that point.
Parameters
offset- the byte offset to seek to, from the start of
Returns Ok on success, or why the seek
Examples
Reading a length-prefixed record, then rewinding past it
let length = handle.readLine.or("0").to(Integer).or(0)
let record = handle.readBytes.try
handle.seek(0)
reset
reset : Result<Void, ReadError>
Moves the handle's cursor back to the start of the file — the same as seek(0), for the common case of re-reading a handle from the top.
Returns Ok on success, or why the reset
Examples
Reading a file twice
let firstPass = handle.read.or("")
handle.reset
let secondPass = handle.read.or("")
On FileHandle<CanRead, W>
getLine Readable
getLine : Result<String?, ReadError>
Reads the next line from the handle, without its newline.
Answers None at end of file, which is what makes it usable as a loop condition. The same operation as readLine, under the name IO.getLine uses.
Returns the next line, or None at end of file
Examples
Walking a file line by line
foul echo(handle: FileHandle<CanRead, W>) -> Void do
match handle.getLine do
Just(line) => do
IO.printLine(line)
echo(handle)
end
None => ()
end
end
get Readable
get : Result<String?, ReadError>
Reads a single character from the handle, as a one-character String.
Answers None at end of file.
Returns the next character, or None at end of file
Examples
let firstChar = handle.get.or("")
readLine Readable
readLine : Result<String?, ReadError>
Reads the next line from the handle, without its newline. The same as getLine, named for reading from a file rather than from a console.
Returns the next line, or None at end of file
Examples
let header = handle.readLine.or("")
Reading the first three lines
let head = (1..3).items.map { |_| handle.readLine.or("") }
read Readable
read : Result<String, ReadError>
Reads everything remaining in the file and returns it as one String.
Reads from the current position, so calling it after a readLine gives the rest of the file rather than the whole of it.
Returns the remaining contents
Examples
let body = handle.read.or("")
Skipping a header line, then taking the rest
handle.readLine
let body = handle.read.or("")
readBytes Readable
readBytes : Result<Binary, ReadError>
Reads all remaining bytes from the handle without decoding them.
Unlike read, this accepts arbitrary binary data and cannot fail because the input is not valid UTF-8. It starts at the handle's current position.
Returns the remaining bytes
Examples
Reading a file with an unknown encoding
let payload = handle.readBytes.try
eof? Readable
eof? : Bool
Returns true when the handle has reached the end of the file.
Returns true at end of file
Examples
handle.eof? # => false, before anything has been read
atEnd? Readable
atEnd? : Bool
Returns true when the handle has reached the end of the file. The same as eof?, spelled out.
Returns true at end of file
Examples
if !handle.atEnd?
IO.printLine(handle.readLine.or(""))
end
feed
feed : Feed<String>?
Returns the handle's remaining lines as a lazy Feed.
Lines are read on demand off the handle's own position, so this is how to look at the start of a very large file, or process one without holding it all in memory. The feed shares the handle's cursor: interleaving readLine with it advances one position through one open file.
The feed ends at the last line, so taking more lines than the file has answers just the lines there are.
NOT part of Readable: a feed is neither pure nor reusable, so requiring it of every Readable would put a foul, one-shot operation on types that have no such cursor to offer. It stays a FileHandle method.
Returns the lines as a feed, or None
Examples
The first ten lines of a large file
handle.feed
.map { |lines| lines.take(10) }
.or([])
On FileHandle<R, CanWrite>
writeBytes Writable
writeBytes(content: Binary) -> Void
Writes content verbatim, without text encoding or a trailing newline.
Parameters
content- the bytes to write
Examples
Copying an opaque payload
destination.writeBytes(source.readBytes.try)
printLine Writable
printLine(content: Showable) -> Void
Writes content followed by a newline.
Parameters
content- the value to write
Examples
Writing a report line by line
rows.each { |row| handle.printLine(row) }
print Writable
print(content: Showable) -> Void
Writes content with no trailing newline.
Parameters
content- the value to write
Examples
Building a line from pieces
handle.print("name,")
handle.print("age")
handle.printLine("")