skills/fp-rust/SKILL.md
Functional Programming in Rust — opinionated guidelines for writing idiomatic, FP-first Rust code. Use this skill whenever writing, reviewing, refactoring, or scaffolding Rust code. Covers: functional core / imperative shell architecture, immutability-first patterns, pure functions, algebraic data types, pipeline-oriented programming with iterators and Result chains, error handling as data, dependency rejection over dependency injection, type-driven design (parse don't validate, typestate), and concurrency via message passing. Also use when the user asks about Rust project structure, Rust error handling patterns, Rust iterator pipelines, Cargo.toml setup for a new Rust project, or Rust code review. Apply these guidelines even when the user doesn't explicitly mention "functional programming" — this is the default coding style for all Rust work.
npx skillsauth add mpurbo/purbo-skills fp-rustInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Write Rust that is idiomatic, readable, and practical while maximizing FP principles. Rust is not Haskell — ownership IS the type-level effect system. Let FP emerge naturally from Rust's idioms: ownership, borrowing, iterators, enums, and traits.
For detailed patterns, code examples, library guidance, and rationale, read the
companion reference:
cat ${SKILL_PATH}/references/FP_RUST_GUIDELINES.md
Load the reference when you need: specific code examples for a pattern, library selection guidance (Appendix A/C), Clippy configuration (Appendix D), or the FP ↔ Rust concept map (Appendix B).
Every program is a pure core surrounded by a thin imperative shell.
Core (in src/core/):
Result/Option as decisions)Shell (in src/shell/ and src/main.rs):
Litmus test: If core/ imports std::io or tokio, it belongs in shell/.
src/
├── main.rs # Shell: wiring, IO, entry point
├── shell/ # Adapters: HTTP, DB, FS, CLI
├── core/ # Pure domain logic — NO std::fs, NO tokio
│ ├── types.rs # ADTs, newtypes, domain models
│ ├── transform.rs # Pure transformations
│ └── validate.rs # Validation combinators
└── lib.rs # Re-exports core
Apply these in order of priority when writing or reviewing Rust code:
&T) for reads — zero cost, pure&mut T) only as last resort, encapsulatedPass Copy types (i32, bool, f64, Duration) by value, not reference.
Accept &str over String, &[T] over Vec<T> in parameters.
Defer .to_string(), .collect() until the last possible moment.
A function is pure if: same inputs → same output, no side effects. When a function needs something impure (time, random), inject it as a parameter:
// ❌ fn is_expired(token: &Token) -> bool { token.expires_at < SystemTime::now() }
// ✅ fn is_expired(token: &Token, now: SystemTime) -> bool { token.expires_at < now }
The signature tells the full story: no &mut self, no &dyn SomeService, no global state.
enum (sum types) to model possibilities — not flag fields or stringly-typed statusstruct (product types) with private fields and constructorsstruct UserId(Uuid), struct Amount(Decimal)_ => that silently swallows future variantsDefault to iterator chains (.iter().map().filter().collect()), not for loops.
Use ? operator and .and_then() for Result chains (railway-oriented programming).
Return impl Iterator<Item = T> over Vec<T> when possible — defer .collect().
Preference order:
.iter().map().filter().collect() > for + match (no mut) > for + mut accumulator
thiserroranyhow::Result in shell, typed errors in coreunwrap()/expect() in core — propagate with ?panic! for expected conditionsPass data in, get data out. Don't inject &dyn Repository — instead:
Vec<Command> for effects)Trait abstraction only when genuinely multiple runtime backends (not "for testing").
Arc<T> (immutable sharing) over Arc<Mutex<T>> (mutable sharing)mpsc, oneshot, broadcast) for coordinationrayon::par_iter() for CPU-bound parallel computationRun through when writing or reviewing any function:
&T, &str, &[T]. Copy types by value.mut? → Replace with transform/fold/map. If needed, encapsulate.unwrap()? → Only in shell/test/provably safe.String → newtype? Option → separate type? bool → enum?.iter() chains or .and_then(). Defer .collect()./// on public items, comments explain "why" not "what".mut Concession Litmus TestBefore using mut in core code:
Acceptable concessions: performance-critical inner loops (with profiling evidence),
builder patterns (produced value is immutable), complex fold readability,
OnceCell/LazyLock for memoization, tracing for diagnostics only.
fn(mut self) -> SelfThis is NOT impure mutation — it's a value-to-value transform where Rust reuses memory. The caller passes ownership in and gets a new value out:
fn with_discount(mut order: Order, pct: f64) -> Order {
order.total *= 1.0 - pct;
order
}
Don't clone the world just to "look functional."
[dependencies]
itertools = "0.14" # Extended pipeline combinators
tap = "1" # .pipe() and .tap() for pipeline readability
derive_more = { version = "1", features = ["full"] } # Newtype ergonomics
thiserror = "2" # Domain error enums
serde = { version = "1", features = ["derive"] }
rust_decimal = "1" # Financial math (no floats)
anyhow = "1" # Shell error handling
tokio = { version = "1", features = ["full"] } # Shell async runtime
tracing = "0.1" # Structured logging
For library evaluation, conditional crates (frunk, imbl, rayon, proptest), Clippy configuration, and the full FP ↔ Rust concept map, consult the reference document.
Flag these patterns and suggest FP alternatives:
| Smell | Suggest |
|-------|---------|
| let mut for accumulation | .fold() or .map().collect() |
| for loop pushing into Vec | Iterator pipeline |
| &dyn Trait in core for testability | Dependency rejection |
| unwrap() in core/library code | ? or explicit error handling |
| String/bool for domain states | Enum (sum type) or newtype |
| Arc<Mutex<T>> | Channels or Arc<T> immutable snapshot |
| IO in core functions | Move to shell, pass data in |
| _ => catch-all in match | Exhaustive match with explicit variants |
| Nested if-let for Option/Result | .map(), .and_then(), ? pipeline |
| .clone() to satisfy borrow checker | Restructure lifetimes, or use &T |
development
Use when turning a user's early brain-dump, product idea, architecture prompt, or "write a high-level system spec" request into a System Spec markdown document. This is Step 1 of the spec-driven workflow and should be used alongside superpowers:brainstorming when available. It decomposes the system into independently developable subsystems, defines contract boundaries, maps subsystem dependencies and parallelization order, and includes Mermaid dependency diagrams. Use this before subsystem-design-spec and before any OpenSpec artifacts. Trigger on phrases like "high-level system spec", "system specification", "system spec", "brain-dump to spec", "turn this into a spec", "write the spec", "architecture spec", "identify subsystems", "subsystem boundaries", "subsystem dependencies", "contract boundaries", "input/output contracts", "development order", "parallel development", "dependency map", "mermaid diagram", "docs/spec/system-spec.md", and any request to create a System Spec for Step 1 Brainstorming.
development
Use when creating or iterating on a detailed per-subsystem technical design specification from a system spec, before starting OpenSpec workflow. Triggers: "design spec", "subsystem spec", "write the spec for S1", "phase breakdown", "implementation phases", "mid-level spec", "technical design". Encodes opinionated progressive phase discipline with FP progression and contract boundaries. Do NOT use for high-level system specs (use brainstorming) or for OpenSpec artifacts (use openspec directly).
development
Use when setting up or updating OpenSpec's config.yaml for a project, or when OpenSpec workflow isn't picking up development disciplines (TDD, progressive phases, FP conventions). Triggers: "configure openspec", "setup openspec", "openspec config", "why didn't openspec use TDD", "openspec not invoking skills", "grounding config". Generates config.yaml with project context and skill invocation rules across sessions. Do NOT use for non-OpenSpec projects or for general CLAUDE.md configuration.
documentation
Apply consistent pastel color styling to mermaid diagrams. Use whenever creating or editing mermaid diagrams in documentation, specs, or PRDs.