IO Effects
Working with input/output effects in Sounio
IO Effects
The IO effect represents input/output operations—reading files, printing to console, network requests, and more.
Why IO Effects?
In pure functional programming, functions should always return the same output for the same input. IO operations violate this:
// This function's result depends on external state
fn read_temp() -> f64 with IO {
sensor.read() // Different result each time!
}
By marking IO explicitly, Sounio helps you:
- Track which functions interact with the outside world
- Isolate side effects for testing
- Reason about program behavior
Basic IO Operations
Console Output
fn greet(name: String) with IO {
print("Hello, " + name + "!")
println("Welcome to Sounio")
}
Console Input
fn get_name() -> String with IO {
print("Enter your name: ")
read_line()
}
File Operations
fn process_file(path: String) -> String with IO {
let content = read_file(path)
let processed = transform(content)
write_file(path + ".out", processed)
processed
}
IO Effect Operations
The IO effect provides these operations:
| Operation | Description |
|---|---|
print(s) | Print without newline |
println(s) | Print with newline |
read_line() | Read line from stdin |
read_file(path) | Read entire file |
write_file(path, content) | Write to file |
append_file(path, content) | Append to file |
file_exists(path) | Check if file exists |
delete_file(path) | Delete a file |
Performing IO
Use perform for explicit effect operations:
fn log_warning(msg: String) with IO {
perform IO::warn(msg)
}
fn log_error(msg: String) with IO {
perform IO::error(msg)
}
Combining with Other Effects
IO often combines with other effects:
fn read_config() -> Config with IO, Alloc {
let content = read_file("config.json")
let config = parse_json(content) // Allocates
config
}
fn update_database(data: Data) with IO, Mut {
let connection = connect()
connection.execute(data) // Mutates connection state
}
IO and Error Handling
IO operations can fail. Use Result for error handling:
fn safe_read(path: String) -> Result<String, IOError> with IO {
if !file_exists(path) {
return Err(IOError::NotFound(path))
}
Ok(read_file(path))
}
Or use the ? operator:
fn process_all(paths: Vec<String>) -> Result<Vec<Data>, IOError> with IO {
let mut results = vec![]
for path in paths {
let content = read_file(path)? // Propagates errors
results.push(parse(content))
}
Ok(results)
}
Testing IO Code
Sounio’s effect handlers let you mock IO for testing:
#[test]
fn test_processor() {
// Create mock IO handler
let mock_io = MockIO {
files: HashMap::from([
("input.txt", "test data"),
]),
}
// Run with mock
let result = handle mock_io {
process_file("input.txt")
}
assert_eq(result, "PROCESSED: test data")
}
Async IO
For non-blocking IO, combine with the Async effect:
fn fetch_all(urls: Vec<String>) -> Vec<Response> with IO, Async {
let futures = urls.map(|url| async { fetch(url) })
await_all(futures)
}
Best Practices
- Minimize IO surface: Keep IO at the edges of your program
- Handle errors: IO operations can always fail
- Use buffering: For performance with many small operations
- Close resources: Use RAII patterns for file handles