Custom Effects
Define your own algebraic effects in Sounio
Custom Effects
Beyond built-in effects, Sounio lets you define custom algebraic effects for domain-specific needs.
Defining Effects
Declare an effect with its operations:
effect Logger {
fn log(level: LogLevel, message: String)
fn get_log_level() -> LogLevel
}
effect Random {
fn random() -> f64
fn random_range(min: i32, max: i32) -> i32
}
Using Custom Effects
Functions can require custom effects:
fn simulate_experiment() -> f64 with Random {
let noise = perform Random::random() * 0.1
let base_value = 42.0
base_value + noise
}
Multiple custom effects can be combined:
fn run_trial(id: i32) with Random, Logger, IO {
perform Logger::log(Info, "Starting trial " + id.to_string())
let result = simulate_experiment()
print("Result: " + result.to_string())
perform Logger::log(Debug, "Trial completed")
}
Effect Handlers
Handle effects with custom implementations:
handler ConsoleLogger for Logger {
var min_level: LogLevel = Info
fn log(level: LogLevel, message: String) {
if level >= min_level {
println("[" + level.to_string() + "] " + message)
}
}
fn get_log_level() -> LogLevel {
min_level
}
}
handler SeededRandom for Random {
var seed: u64
fn random() -> f64 {
seed = lcg_next(seed)
(seed as f64) / (u64::MAX as f64)
}
fn random_range(min: i32, max: i32) -> i32 {
min + (random() * (max - min) as f64) as i32
}
}
Running with Handlers
Use handle to run code with specific handlers:
fn main() with IO {
let logger = ConsoleLogger { min_level: Debug }
let rng = SeededRandom { seed: 12345 }
handle logger, rng {
run_trial(1)
run_trial(2)
run_trial(3)
}
}
Effect Polymorphism
Write functions generic over effect implementations:
fn run_simulation<R: Random>() -> Vec<f64> with R {
(0..100).map(|_| perform R::random()).collect()
}
Resumable Effects
Effects can be resumable (multi-shot):
effect Choice {
fn choose<T>(options: Vec<T>) -> T
}
handler AllChoices for Choice {
fn choose<T>(options: Vec<T>) -> T {
// Resume with each option, collecting all results
options.flat_map(|opt| resume(opt))
}
}
This enables:
fn enumerate_paths() -> Vec<Path> with Choice {
let first = perform Choice::choose(vec!["A", "B"])
let second = perform Choice::choose(vec!["1", "2", "3"])
vec![Path::new(first, second)]
}
// With AllChoices handler, returns all 6 combinations
Domain-Specific Effects
Scientific Computing
effect Measurement {
fn read_sensor(id: SensorId) -> Knowledge<f64>
fn calibrate(id: SensorId)
}
effect Experiment {
fn start_trial(params: TrialParams)
fn record_observation(data: Knowledge<f64>)
fn end_trial() -> TrialResult
}
Machine Learning
effect Gradient {
fn forward(x: Tensor) -> Tensor
fn backward(grad: Tensor)
fn get_parameters() -> Vec<Tensor>
}
Probabilistic Programming
effect Prob {
fn sample(dist: Distribution) -> f64
fn observe(dist: Distribution, value: f64)
fn factor(log_weight: f64)
}
Combining Effects
Effects compose naturally:
fn bayesian_experiment() with Prob, Measurement, Logger {
// Sample prior
let mu = perform Prob::sample(Normal(0.0, 1.0))
// Take measurement
let obs = perform Measurement::read_sensor(SENSOR_A)
// Condition on observation
perform Prob::observe(Normal(mu, obs.uncertainty), obs.value)
perform Logger::log(Info, "Posterior updated")
mu
}
Best Practices
- Keep effects focused: One effect, one responsibility
- Use semantic names:
LoggernotSideEffect1 - Document operations: Explain what each operation does
- Provide default handlers: Make it easy to get started
- Consider resumption: Multi-shot vs single-shot semantics