Stillwater
A Rust library for pragmatic effect composition and validation, emphasizing the pure core, imperative shell pattern.
Philosophy
Stillwater embodies a simple idea:
- Pure functions (unchanging, referentially transparent)
- Effects (flowing, performing I/O)
Keep your business logic pure and calm like still water. Let effects flow at the boundaries.
What Problems Does It Solve?
1. "I want ALL validation errors, not just the first one"
use Validation;
// Standard Result: stops at first error
let email = validate_email?; // Stops here
let age = validate_age?; // Never reached if email fails
// Stillwater: accumulates all errors
let user = all?;
// Returns: Err(vec![EmailError, AgeError, NameError])
2. "How do I validate that all items have the same type before combining?"
use validate_homogeneous;
use discriminant;
// Without validation: runtime panic
let mixed = vec!;
// items.into_iter().reduce(|a, b| a.combine(b)) // PANIC!
// With validation: type-safe error accumulation
let result = validate_homogeneous;
match result
3. "How do I test code with database calls?"
use *;
// Pure business logic (no DB, easy to test)
// Effects at boundaries (mockable) - zero-cost by default
// Test with mock environment
async
4. "I need to fetch multiple independent resources"
use *;
// Combine independent effects - neither depends on the other
// Or use zip3 for cleaner flat tuples
// Combine results with a function directly using zip_with
let effect = fetch_price
.zip_with;
5. "My errors lose context as they bubble up"
use *;
fetch_user
.context
.and_then
.context
.run.await?;
// Error output:
// Error: UserNotFound(12345)
// -> Loading user profile
// -> Processing user data
6. "I need clean dependency injection without passing parameters everywhere"
use *;
// Functions don't need explicit config parameters
let config = Config ;
let result = fetch_with_extended_timeout.run.await?;
// Uses timeout=60 without changing the original config
7. "I need guaranteed cleanup even when errors occur"
use ;
use *;
// Single resource with guaranteed cleanup
let result = bracket.run.await;
// Multiple resources with LIFO cleanup order
let result = bracket2.run.await;
// Fluent builder for ergonomic multi-resource management
let result = acquiring
.and
.and
.with_flat3
.run
.await;
// Explicit error handling with BracketError
let result = bracket_full.run.await;
match result
8. "Retry logic is scattered and hard to test"
use ;
use Duration;
// Stillwater: Policy as Data
// Define retry policies as pure, testable values
let api_policy = exponential
.with_max_retries
.with_max_delay
.with_jitter;
// Test the policy without any I/O
assert_eq!;
assert_eq!;
assert_eq!;
// Reuse the same policy across different effects
retry;
retry;
// Conditional retry: only retry transient failures
retry_if;
// Observability: hook into retry events for logging/metrics
retry_with_hooks;
9. "I want the type system to prevent resource leaks"
use *;
// Mark effects with resource acquisition at the TYPE level
// The bracket pattern guarantees resource neutrality
// Transaction protocols enforced at compile time
// This function MUST be resource-neutral or it won't compile
// Zero runtime overhead - all tracking is compile-time only!
Core Features
Validation<T, E>- Accumulate all errors instead of short-circuiting- Predicate combinators - Composable validation logic with
and,or,not,all_of,any_of- String predicates:
len_between,contains,starts_with,all_chars, etc. - Number predicates:
between,gt,lt,positive,negative, etc. - Collection predicates:
all,any,has_len,is_empty, etc. - Seamless integration with
Validationviaensure()andvalidate()
- String predicates:
- Validation combinators - Declarative validation with
ensurefamily (replaces verboseand_thenboilerplate)Effect:.ensure(),.ensure_with(),.ensure_pred(),.unless(),.filter_or()Validation:.ensure(),.ensure_fn(),.ensure_with(),.ensure_fn_with(),.unless(),.filter_or()- Zero-cost: compiles to concrete types with no heap allocation
- Reduces 12-line validation blocks to single-line predicates
NonEmptyVec<T>- Type-safe non-empty collections with guaranteed head elementEffecttrait - Zero-cost effect composition following thefuturescrate pattern- Zero heap allocations by default
- Explicit
.boxed()when type erasure is needed - Returns
impl Effectfor optimal performance
- Zip combinators - Combine independent effects into tuples
zip(),zip_with()methods for pairwise combinationzip3()throughzip8()for flat tuple results- Zero-cost: all combinators return concrete types
- Parallel effect execution - Run independent effects concurrently
- Zero-cost:
par2(),par3(),par4()for heterogeneous effects - Boxed:
par_all(),par_try_all(),race(),par_all_limit()for homogeneous collections
- Zero-cost:
- Retry and resilience - Policy-as-data approach with exponential, linear, constant, and Fibonacci backoff. Includes jitter, conditional retry, retry hooks, and timeout support
- Error recovery - Selective error handling with predicate-based recovery
recover(),recover_with(),recover_some()for conditional error recoveryfallback(),fallback_to()for default values and alternative effects- Predicate composition for sophisticated recovery strategies
- Real-world patterns: multi-tier caching, graceful degradation, API fallback
- Resource management - Comprehensive bracket pattern for safe acquire/use/release
bracket(),bracket2(),bracket3()for single and multiple resources with LIFO cleanupbracket_full()returnsBracketErrorwith explicit error handling for all failure modesacquiring()builder for fluent multi-resource management withwith_flat2/3/4- Guaranteed cleanup even on errors, partial acquisition rollback
- Compile-time resource tracking - Type-level resource safety with zero runtime overhead
- Resource markers:
FileRes,DbRes,LockRes,TxRes,SocketRes(or define custom) ResourceEffecttrait withAcquires/Releasesassociated types- Extension methods:
.acquires::<R>(),.releases::<R>(),.neutral() resource_bracketfor guaranteed resource-neutral operationsassert_resource_neutralfor compile-time leak detection
- Resource markers:
- Traverse and sequence - Transform collections with
traverse()andsequence()for both validations and effects - Reader pattern helpers - Clean dependency injection with
ask(),asks(), andlocal() Semigrouptrait - Associative combination of values- Extended implementations for
HashMap,HashSet,BTreeMap,BTreeSet,Option - Wrapper types:
First,Last,Intersectionfor alternative semantics
- Extended implementations for
Monoidtrait - Identity elements for powerful composition patterns- Testing utilities - Ergonomic test helpers
MockEnvbuilder for composing test environments- Assertion macros:
assert_success!,assert_failure!,assert_validation_errors! TestEffectwrapper for deterministic effect testing- Optional
proptestfeature for property-based testing
- Context chaining - Never lose error context
- Tracing integration - Instrument effects with semantic spans using the standard
tracingcrate - Zero-cost abstractions - Follows
futurescrate pattern: concrete types, no allocation by default - Works with
?operator - Integrates with Rust idioms - No heavy macros - Clear types, obvious behavior
Quick Start
use *;
// 1. Validation with error accumulation
// 2. Effect composition (zero-cost by default)
// 3. Run at application boundary
let env = AppEnv ;
let result = create_user.run.await?;
Zero-Cost Effect System
Version 0.11.0 introduces a zero-cost effect system following the futures crate pattern:
// Free-standing constructors (not methods)
let effect = pure; // Not Effect::pure(42)
let effect = fail; // Not Effect::fail("error")
let effect = from_fn; // Not Effect::from_fn(...)
// Chain combinators - each returns a concrete type, zero allocations
let result = pure
.map
.and_then
.map;
// Use .boxed() when you need type erasure
// Collections of effects require boxing
let effects: = vec!;
let results = par_all.await;
When to use .boxed():
- Storing effects in collections (
Vec<BoxedEffect<...>>) - Returning different effect types from branches
- Recursive effect definitions
- Dynamic dispatch scenarios
When NOT to use .boxed():
- Simple linear chains (use
impl Effect) - Fixed combinator sequences
- Performance-critical paths
Why Stillwater?
Compared to existing solutions:
vs. frunk:
- Focused on practical use cases, not type-level programming
- Better documentation and examples
- Effect composition, not just validation
vs. monadic:
- No awkward macro syntax (
rdrdo! { ... }) - Zero-cost by default (follows
futurescrate pattern) - Idiomatic Rust, not Haskell port
vs. hand-rolling:
- Validation accumulation built-in
- Error context handling
- Testability patterns established
- Composable, reusable
What makes it "Rust-first":
- No attempt at full monad abstraction (impossible without HKTs)
- Works with
?operator viaTrytrait - Zero-cost via concrete types and monomorphization (like
futures) - Integrates with async/await
- Borrows checker friendly
- Clear error messages
Installation
Add to your Cargo.toml:
[]
= "0.11"
# Optional: async support
= { = "0.11", = ["async"] }
# Optional: tracing integration
= { = "0.11", = ["tracing"] }
# Optional: jitter for retry policies
= { = "0.11", = ["jitter"] }
# Optional: property-based testing
= { = "0.11", = ["proptest"] }
# Multiple features
= { = "0.11", = ["async", "tracing", "jitter"] }
Examples
Run any example with cargo run --example <name>:
| Example | Demonstrates |
|---|---|
| predicates | Composable predicate combinators for validation logic |
| form_validation | Validation error accumulation |
| homogeneous_validation | Type-safe validation for discriminated unions before combining |
| nonempty | NonEmptyVec type for guaranteed non-empty collections |
| user_registration | Effect composition and I/O separation |
| error_context | Error trails for debugging |
| data_pipeline | Real-world ETL pipeline |
| testing_patterns | Testing pure vs effectful code |
| reader_pattern | Reader pattern with ask(), asks(), and local() |
| validation | Validation type and error accumulation patterns |
| effects | Effect type and composition patterns |
| parallel_effects | Parallel execution with par_all, race, and par_all_limit |
| recover_patterns | Error recovery with recover, recover_with, recover_some, fallback patterns |
| retry_patterns | Retry policies, backoff strategies, timeouts, and resilience patterns |
| io_patterns | IO module helpers for reading/writing |
| pipeline | Data transformation pipelines |
| traverse | Traverse and sequence for collections of validations and effects |
| monoid | Monoid and Semigroup traits for composition |
| extended_semigroup | Semigroup for HashMap, HashSet, Option, and wrapper types |
| tracing_demo | Tracing integration with semantic spans and context |
| boxing_decisions | When to use .boxed() vs zero-cost effects |
| resource_scopes | Bracket pattern for safe resource management with guaranteed cleanup |
| resource_tracking | Compile-time resource tracking with type-level safety |
See examples/ directory for full code.
Production Readiness
Status: 0.11.0 - Production Ready
- 355 unit tests passing
- 113 documentation tests passing
- 21 runnable examples
- Zero clippy warnings
- Full async support
- CI/CD pipeline with security audits
This library is stable and ready for use. The 0.x version indicates the API may evolve based on community feedback.
Migration from 0.10.x
Version 0.11.0 introduces breaking changes with the zero-cost effect system. See MIGRATION.md for detailed upgrade instructions.
Key changes:
// Before (0.10.x)
pure
fail
from_fn
// After (0.11.0)
pure
fail
from_fn
// Return types changed
// Boxed by default
..> // Zero-cost by default
// Explicit boxing
Documentation
- User Guide - Comprehensive tutorials
- API Docs - Full API reference
- FAQ - Common questions
- Design - Architecture and decisions
- Philosophy - Core principles
- Patterns - Common patterns and recipes
- Comparison - vs other libraries
- Migration Guide - Upgrading from 0.10.x to 0.11.0
Migrating from Result
Already using Result everywhere? No problem! Stillwater integrates seamlessly:
// Your existing code works as-is
// Upgrade to accumulation when you need it
// Convert back to Result when needed
let result: = validation.into_result;
Start small, adopt progressively. Use Validation only where you need error accumulation.
Contributing
Contributions welcome! This is a young library with room to grow:
- Bug reports and feature requests via issues
- Documentation improvements
- More examples and use cases
- API feedback and design discussions
Before submitting PRs, please open an issue to discuss the change.
Ecosystem
Stillwater is part of a family of libraries that share the same functional programming philosophy:
| Library | Description |
|---|---|
| premortem | Configuration validation that finds all errors before your app runs. Multi-source loading with error accumulation and value origin tracing. |
| postmortem | Validation library that accumulates all errors with precise JSON path tracking. Composable schemas, cross-field validation, and effect integration. |
| mindset | Zero-cost, effect-based state machines. Pure guards for validation, explicit actions for side effects, environment pattern for testability. |
All libraries emphasize:
- Error accumulation over short-circuiting
- Pure core, effects at the boundaries
- Zero-cost abstractions
- Testability through dependency injection
License
MIT © Glen Baker iepathos@gmail.com
"Like a still pond with water flowing through it, stillwater keeps your pure business logic calm and testable while effects flow at the boundaries."