Skip to main content
Klar

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

MethodDescription
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 written

BufReader Methods

MethodDescription
BufReader.new(file)Create buffered reader
.read_line()Read next line
.read_bytes(n)Read n bytes
.close()Close underlying file

BufWriter Methods

MethodDescription
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

MethodDescription
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 returns None to 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_dir returns a List#[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_dir repeatedly, be aware of this limitation. A future version of Klar will implement proper nested cleanup for List#[String].

Filesystem Function Reference

FunctionDescription
fs_exists(path) -> boolCheck if path exists
fs_is_file(path) -> boolCheck if path is a regular file
fs_is_dir(path) -> boolCheck 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