Skip to main content
Background Image
  1. Projects/

Stillwater - Pure Core, Imperative Shell for Rust

Author
Glen Baker
Building open source tooling
Table of Contents

Keep business logic pure and calm like still water, let effects flow at the boundaries

Crates.io
Documentation
GitHub

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 - Result short-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#

Related

Debtmap - Rust Technical Debt Analyzer
Prodigy - AI Workflow Orchestration for Claude
Automating Documentation Maintenance with Prodigy: A Real-World Case Study