I/O Types
Klar provides types for file and stream I/O operations.
File
The File type represents an open file handle.
Opening Files
// Open for reading
let file: Result#[File, IoError] = File.open("data.txt")
// Open for writing (creates or truncates)
let file: Result#[File, IoError] = File.create("output.txt")
// Open for appending
let file: Result#[File, IoError] = File.append("log.txt")Reading Files
fn read_file_contents(path: string) -> Result#[string, IoError] {
let file: File = File.open(path)?
let contents: string = file.read_to_string()?
file.close()
return Ok(contents)
}Writing Files
fn write_to_file(path: string, content: string) -> Result#[(), IoError] {
let file: File = File.create(path)?
file.write(content)?
file.close()
return Ok(())
}File Methods
| Method | Description |
|---|---|
File.open(path) | Open file for reading |
File.create(path) | Create/truncate file for writing |
File.append(path) | Open file for appending |
.read_to_string() | Read entire file as string |
.read_bytes() | Read entire file as bytes |
.write(string) | Write string to file |
.write_bytes(bytes) | Write bytes to file |
.close() | Close the file handle |
Example: Copy File
fn copy_file(src: string, dst: string) -> Result#[(), IoError] {
let content: string = File.open(src)?.read_to_string()?
File.create(dst)?.write(content)?
return Ok(())
}Standard Streams
Stdin
Read from standard input:
fn read_line() -> Result#[string, IoError] {
return Stdin.read_line()
}
fn main() -> i32 {
print("Enter your name: ")
let name: string = Stdin.read_line() ?? ""
println("Hello, {name}!")
return 0
}Stdout
Write to standard output:
fn main() -> i32 {
Stdout.write("Hello, ")
Stdout.write("World!")
Stdout.write("\n")
return 0
}
// More commonly, use println:
fn main() -> i32 {
println("Hello, World!")
return 0
}Stderr
Write to standard error:
fn log_error(message: string) -> void {
Stderr.write("ERROR: ")
Stderr.write(message)
Stderr.write("\n")
}Buffered I/O
BufReader
Buffered reading for efficient I/O:
let file: File = File.open("large_file.txt")?
let reader: BufReader = BufReader.new(file)
// Read line by line
while true {
let line: ?string = reader.read_line()
match line {
Some(l) => { process_line(l) }
None => { break } // EOF
}
}BufWriter
Buffered writing for efficient I/O:
let file: File = File.create("output.txt")?
let writer: BufWriter = BufWriter.new(file)
for i: i32 in 0..1000 {
writer.write("Line {i}\n")
}
writer.flush() // Ensure all data is writtenBufReader Methods
| Method | Description |
|---|---|
BufReader.new(file) | Create buffered reader |
.read_line() | Read next line |
.read_bytes(n) | Read n bytes |
.close() | Close underlying file |
BufWriter Methods
| Method | Description |
|---|---|
BufWriter.new(file) | Create buffered writer |
.write(string) | Write string to buffer |
.write_bytes(bytes) | Write bytes to buffer |
.flush() | Flush buffer to file |
.close() | Flush and close |
IoError
I/O operations return Result#[T, IoError]:
enum IoError {
NotFound,
PermissionDenied,
AlreadyExists,
InvalidData,
UnexpectedEof,
Other(string),
}Handling I/O Errors
fn read_config() -> Result#[string, string] {
let result: Result#[File, IoError] = File.open("config.txt")
match result {
Ok(file) => {
return file.read_to_string().map_err(|e: IoError| -> string {
return "failed to read: {e}"
})
}
Err(IoError.NotFound) => {
return Err("config file not found")
}
Err(IoError.PermissionDenied) => {
return Err("no permission to read config")
}
Err(e) => {
return Err("unexpected error: {e}")
}
}
}Example: Line-by-Line Processing
fn process_log_file(path: string) -> Result#[i32, IoError] {
let file: File = File.open(path)?
let reader: BufReader = BufReader.new(file)
var error_count: i32 = 0
loop {
let line: ?string = reader.read_line()
match line {
Some(l) => {
if l.contains("ERROR") {
error_count = error_count + 1
println("Found error: {l}")
}
}
None => { break }
}
}
reader.close()
return Ok(error_count)
}Example: Writing Structured Data
fn write_csv(path: string, data: List#[(string, i32)]) -> Result#[(), IoError] {
let file: File = File.create(path)?
let writer: BufWriter = BufWriter.new(file)
// Write header
writer.write("name,value\n")
// Write data rows
for (name, value) in data {
writer.write("{name},{value}\n")
}
writer.flush()
writer.close()
return Ok(())
}Example: Interactive Input
fn interactive_menu() -> i32 {
loop {
println("Menu:")
println("1. Option A")
println("2. Option B")
println("3. Exit")
print("Choice: ")
let input: string = Stdin.read_line() ?? ""
let choice: ?i32 = input.trim().to#[i32]
match choice {
Some(1) => { println("Selected A") }
Some(2) => { println("Selected B") }
Some(3) => { return 0 }
_ => { println("Invalid choice") }
}
println("")
}
}Path Type
The Path type represents a filesystem path and provides methods for path manipulation.
Creating Paths
let p: Path = Path.new("/home/user/documents")Path Methods
let p: Path = Path.new("/home/user/file.txt")
// Convert to string
let s: string = p.to_string() // "/home/user/file.txt"
// Join path components (handles trailing slashes correctly)
let joined: Path = p.join("subdir") // "/home/user/file.txt/subdir"
// Get parent directory
let parent: ?Path = p.parent() // Some(Path("/home/user"))
// Get filename
let name: ?string = p.file_name() // Some("file.txt")
// Get extension
let ext: ?string = p.extension() // Some("txt")
// Check existence and type
let exists: bool = p.exists()
let is_file: bool = p.is_file()
let is_dir: bool = p.is_dir()Path Method Reference
| Method | Description |
|---|---|
Path.new(s) | Create path from string |
.to_string() | Convert to string |
.join(other) | Join with another path component |
.parent() | Get parent directory as ?Path |
.file_name() | Get filename component as ?string |
.extension() | Get file extension as ?string |
.exists() | Check if path exists |
.is_file() | Check if path is a regular file |
.is_dir() | Check if path is a directory |
Edge Cases for .parent()
The .parent() method returns None for paths without a directory separator:
| Path | .parent() Result |
|---|---|
"/home/user" | Some(Path("/home")) |
"/home" | Some(Path("/")) |
"/" | Some(Path("/")) — root is its own parent |
"file.txt" | None — no directory separator |
"dir/file.txt" | Some(Path("dir")) |
Note: Unlike some languages that return
"."for paths without a separator, Klar returnsNoneto make it explicit that there is no parent directory component. Use pattern matching to handle this case.
Filesystem Functions
Klar provides standalone functions for common filesystem operations.
Path Queries
// Check if path exists (file or directory)
let exists: bool = fs_exists("/path/to/file")
// Check if path is a file
let is_file: bool = fs_is_file("/path/to/file")
// Check if path is a directory
let is_dir: bool = fs_is_dir("/path/to/directory")Directory Operations
// Create a single directory
let result: Result#[void, IoError] = fs_create_dir("/path/to/new_dir")
// Create directory and all parent directories
let result: Result#[void, IoError] = fs_create_dir_all("/path/to/deep/nested/dir")
// Remove an empty directory
let result: Result#[void, IoError] = fs_remove_dir("/path/to/dir")
// Remove a file
let result: Result#[void, IoError] = fs_remove_file("/path/to/file")File Content
// Read entire file as string
let content: Result#[String, IoError] = fs_read_string("/path/to/file.txt")
// Write string to file (creates or overwrites)
let result: Result#[void, IoError] = fs_write_string("/path/to/file.txt", "content")Reading Directory Contents
let entries: Result#[List#[String], IoError] = fs_read_dir("/path/to/dir")
match entries {
Ok(list) => {
for name: String in list {
println(name)
}
// IMPORTANT: You must manually clean up each string
// before dropping the list. See ownership note below.
list.drop()
}
Err(e) => {
println("Error: {e}")
}
}Ownership for fs_read_dir
Important:
fs_read_dirreturns aList#[String]where:
- The List owns its backing array (freed by
list.drop())- Each String owns its own heap-allocated filename buffer
Current limitation: Calling
list.drop()frees only the List's backing array, not the individual String buffers. This means the filename strings will leak memory.Workaround: For short-lived operations, the memory leak is negligible. For long-running programs that call
fs_read_dirrepeatedly, be aware of this limitation. A future version of Klar will implement proper nested cleanup forList#[String].
Filesystem Function Reference
| Function | Description |
|---|---|
fs_exists(path) -> bool | Check if path exists |
fs_is_file(path) -> bool | Check if path is a regular file |
fs_is_dir(path) -> bool | Check if path is a directory |
fs_create_dir(path) -> Result#[void, IoError] | Create single directory |
fs_create_dir_all(path) -> Result#[void, IoError] | Create directory tree |
fs_remove_file(path) -> Result#[void, IoError] | Delete a file |
fs_remove_dir(path) -> Result#[void, IoError] | Delete empty directory |
fs_read_string(path) -> Result#[String, IoError] | Read file contents |
fs_write_string(path, content) -> Result#[void, IoError] | Write file contents |
fs_read_dir(path) -> Result#[List#[String], IoError] | List directory entries |
Note: Filesystem operations currently support macOS and Linux only. Windows is not implemented.
Best Practices
Always Handle Errors
// Good - handles potential failure
let file: File = File.open(path)?
// or
match File.open(path) {
Ok(f) => { /* use f */ }
Err(e) => { /* handle error */ }
}
// Bad - can panic
let file: File = File.open(path)!Close Files When Done
fn process_file(path: string) -> Result#[(), IoError] {
let file: File = File.open(path)?
// ... do work ...
file.close() // Don't forget!
return Ok(())
}Use Buffered I/O for Large Files
// Good for large files - efficient
let reader: BufReader = BufReader.new(File.open(path)?)
while let Some(line) = reader.read_line() {
process(line)
}
// Less efficient for large files
let content: string = File.open(path)?.read_to_string()?Next Steps
- Error Handling - Working with Result
- Collections - Storing file data
- Primitives - String operations