Stillwater is a Rust library providing pragmatic functional programming abstractions, focused on validation error accumulation and effect composition using the "pure core, imperative shell" pattern.
"Still" represents pure logic (calm, unchanging, referentially transparent). "Water" represents effects (flowing, dynamic, performing I/O). Together: "pure core, imperative shell."
Sort of. Validation is an Applicative Functor, Effect is a Reader/IO monad. But we focus on practical patterns, not category theory. You don't need to understand monads to use Stillwater effectively.
Low. If you understand Result and async/await, you can use Stillwater. The core concepts are:
- Validation accumulates errors (vs Result which short-circuits)
- Effect separates pure logic from I/O (for testability)
Advanced patterns are optional.
Result short-circuits on the first error. Validation accumulates all errors, providing better UX for forms and APIs where users need to see all validation failures at once.
On nightly with try_trait feature, yes! But be aware: ? fails fast (no accumulation). Use Validation::all() for error accumulation. See Try Trait guide.
Use Validation::all_vec() for homogeneous collections, or nest tuples: Validation::all((all1, all2, all3)).
let result: Result<T, E> = validation.into_result();Only if you want to use Validation::all(). Vec<T>, String, and tuples already implement Semigroup. Most of the time you'll use Vec<YourError> which works out of the box.
Effect separates pure logic from I/O, making code more testable and composable. Pure functions need zero mocks! You can test business logic without databases, file systems, or network calls.
For async Effects, yes (or async-std). For sync-only code, no runtime needed.
Yes! Use from_fn() for sync operations. They'll be wrapped in ready futures.
Create simple mock environments (just data structures). Pure functions in your Effects need no mocking. See testing_patterns example.
No! Stillwater follows the futures crate pattern: zero-cost by default. Each combinator returns a concrete type (like Map, AndThen) that the compiler can fully inline. No heap allocations occur for effect chains.
When you need type erasure (collections, recursion, match arms), use .boxed() which allocates once. For I/O-bound work, this is negligible.
Use .boxed() in exactly three cases:
- Collections: Storing different effects in
Vec,HashMap, etc. - Recursion: Breaking infinite type recursion
- Match arms: When different branches return different effect types
// Collections
let effects: Vec<BoxedEffect<i32, String, ()>> = vec![
pure(1).boxed(),
pure(2).map(|x| x * 2).boxed(),
];
// Recursion
fn countdown(n: i32) -> BoxedEffect<i32, String, ()> {
if n <= 0 { pure(0).boxed() }
else { pure(n).and_then(move |_| countdown(n - 1)).boxed() }
}Version 0.11.0 introduced a zero-cost Effect design following the futures crate pattern. The old API boxed every combinator; the new API uses concrete types by default.
Key changes:
Effect::pure(x)→pure(x)Effect<T, E, Env>struct →impl Effect<Output=T, Error=E, Env=Env>trait- Automatic boxing → Explicit
.boxed()when needed
See Migration Guide for detailed upgrade instructions.
See the Migration Guide for step-by-step instructions. Key steps:
- Update imports to use
stillwater::prelude::* - Change return types to
impl Effect<...> - Replace
Effect::pure,Effect::failwithpure,fail - Add
.boxed()where type erasure is needed
No. Use it at I/O boundaries and major operation boundaries where context helps debugging. Don't add context to pure functions or in hot loops.
Yes! Common pattern:
Effect::from_validation(validate_data(data))
.and_then(|valid| save_to_db(valid))Validate first (pure, accumulates errors), then lift to Effect for I/O.
Map them to your error types:
IO::execute(|env| async {
env.db.query()
.await
.map_err(|e| MyError::Database(e.to_string()))
})No! The Effect trait is zero-cost by default:
- Each combinator returns a concrete type (like
Map<AndThen<Pure<...>, F>, G>) - The compiler can fully inline the effect chain
- No heap allocations occur
Validation is just an enum with no overhead. Both compile to efficient code identical to hand-written async functions.
Only when you explicitly call .boxed():
- Storing effects in collections
- Recursive effects
- Match arms with different effect types
For I/O-bound applications (API calls, database queries), boxing overhead is negligible compared to actual work.
Yes! The zero-cost design means you can use Effects in performance-sensitive code. Just avoid .boxed() in the hot path. For tight loops, benchmark to confirm.
Not currently. Effect requires std for async/boxing. Future versions may add no_std support for Validation.
Those are for error handling. Stillwater is for validation (accumulation) and effect composition (separation). Use together! Stillwater for business logic, anyhow for error propagation.
frunk focuses on HLists and type-level programming. Stillwater focuses on practical validation and effects with a lower learning curve.
monadic uses macros for do-notation (rdrdo!). Stillwater uses method chaining (more idiomatic Rust).
Hand-rolling works but requires boilerplate. Stillwater provides tested, composable abstractions that follow best practices.
Result is perfect for operations that should fail fast. Use Result for that! Use Validation when you want ALL errors (forms, API validation). Use Effect when you want testable I/O separation.
See CONTRIBUTING.md. We welcome:
- Bug reports
- Documentation improvements
- Examples
- Feature requests
- Performance improvements
See specs in specs/ directory for planned features. Major upcoming features:
- Parallel effect execution
- More combinators
- Additional examples
- Performance optimizations
Yes! Stillwater 0.1 is stable with comprehensive tests (111 unit tests, 58 doc tests). The 0.x version indicates the API may evolve based on community feedback.
Specify type parameters explicitly on constructor functions:
// Instead of:
let effect = pure(42);
// Do:
let effect = pure::<_, String, ()>(42);You're returning impl Effect but the caller expects a concrete type. Either:
- Use
.boxed()to getBoxedEffect - Update the caller to accept
impl Effect
Use .boxed() for recursive effects:
fn countdown(n: i32) -> BoxedEffect<i32, String, ()> {
if n <= 0 { pure(0).boxed() }
else { pure(n).and_then(move |_| countdown(n - 1)).boxed() }
}Make sure your error type implements Semigroup:
use stillwater::Semigroup;
impl Semigroup for MyError {
fn combine(self, other: Self) -> Self {
// Combine errors
}
}Or use Vec<MyError> which already implements Semigroup.
Check your function signatures. from_fn expects functions returning Result<T, E>, not bare values:
// Wrong:
from_fn(|db: &Db| db.fetch_user(id)) // If fetch_user returns User directly
// Right:
from_fn(|db: &Db| Ok(db.fetch_user(id)))
// Or if fetch_user returns Result:
from_fn(|db: &Db| db.fetch_user(id))- Read the User Guide
- Check PATTERNS.md for recipes
- See examples/ for working code
- Open an issue on GitHub