Keep business logic pure and calm like still water, let effects flow at the boundaries
Links: GitHub | Crates.io | Documentation
Overview#
Stillwater is a Rust library that makes functional programming patterns practical and ergonomic. It implements the “pure core, imperative shell” architecture pattern, keeping business logic pure and testable while pushing I/O and effects to the boundaries of your application.
The Problem#
Rust’s standard library provides excellent building blocks, but certain functional programming patterns remain awkward:
- Error handling stops at first failure -
Resultshort-circuits, making it impossible to collect all validation errors at once - No compile-time non-empty guarantees - Empty collections cause runtime panics in functions that expect data
- I/O mixed with business logic - Testing becomes difficult when side effects are interleaved with pure computation
- Error context gets lost - Stack traces don’t preserve the semantic meaning of where errors originated
- Configuration threading - Passing dependencies through deep call stacks clutters function signatures
The Solution#
Stillwater provides a focused set of types that solve these problems elegantly:
Validation - Error Accumulation#
Unlike Result which stops at the first error, Validation accumulates all errors:
use stillwater::{Validation, Invalid};
fn validate_user(name: &str, age: i32) -> Validation<User, String> {
let name = if name.is_empty() {
Invalid(vec!["Name cannot be empty".into()])
} else {
Validation::valid(name.to_string())
};
let age = if age < 0 {
Invalid(vec!["Age must be positive".into()])
} else {
Validation::valid(age)
};
// Collects ALL errors, not just the first
name.and_then2(age, |n, a| User { name: n, age: a })
}
NonEmptyVec - Compile-Time Guarantees#
Guarantee non-empty collections at the type level:
use stillwater::NonEmptyVec;
// Construction requires at least one element
let items = NonEmptyVec::new(1, vec![2, 3, 4]);
// These methods can never panic
let first = items.first(); // Always exists
let last = items.last(); // Always exists
Effect - Separating Pure Logic from I/O#
The Effect type separates description of effects from their execution:
use stillwater::{Effect, ask};
// Pure function describing what to do
fn fetch_user_data() -> Effect<UserData, Error, Database> {
ask().and_then(|db: &Database| {
// Describe the effect, don't execute it
Effect::from_result(db.query("SELECT * FROM users"))
})
}
// Execute at the boundary
let result = fetch_user_data().run(&real_database);
// Test with mock
let test_result = fetch_user_data().run(&mock_database);
Reader Pattern Helpers#
Clean dependency injection without parameter threading:
use stillwater::{ask, asks, local};
// Access the full environment
let effect = ask::<Config>().map(|config| config.timeout);
// Access a specific field
let timeout = asks::<Config, _>(|c| c.timeout);
// Temporarily modify environment
let with_debug = local(|c: Config| Config { debug: true, ..c }, inner_effect);
Key Features#
- Zero-cost abstractions - Generic types and monomorphization mean no runtime overhead
- Ergonomic API - Works naturally with
?operator and async/await - Async support - Full async compatibility for Effect type
- Parallel execution - Run independent effects concurrently
- Property-based testing - Built-in proptest support for thorough testing
- Comprehensive assertions - Test utilities and assertion macros included
Technical Highlights#
- Language: Rust
- Core Types:
Validation<T, E>,Effect<T, E, Env>,NonEmptyVec<T>,IO<T> - Pattern: Pure core, imperative shell
- Testing: Property-based testing via proptest integration
- Compatibility: Works with standard Rust patterns and error handling
Installation#
# Add to Cargo.toml
cargo add stillwater
# Or with specific features
cargo add stillwater --features async,proptest
Quick Example#
use stillwater::{Validation, NonEmptyVec, Effect, ask};
// Validate input, accumulating all errors
fn validate_order(items: Vec<Item>) -> Validation<NonEmptyVec<Item>, String> {
NonEmptyVec::try_from_vec(items)
.ok_or_else(|| vec!["Order must have at least one item".into()])
.into()
}
// Pure business logic with effect description
fn process_order(order: Order) -> Effect<Receipt, OrderError, PaymentGateway> {
ask().and_then(|gateway: &PaymentGateway| {
Effect::from_result(gateway.charge(order.total))
.map(|confirmation| Receipt::new(order, confirmation))
})
}
Project Status#
Active development, available on crates.io. The library provides stable APIs for production use.
Links#
- GitHub: github.com/iepathos/stillwater
- Documentation: docs.rs/stillwater
- Crates.io: Available via
cargo add stillwater