fp-library
A functional programming library for Rust featuring your favourite higher-kinded types and type classes.
Features
- Higher-Kinded Types (HKT): Implemented using lightweight higher-kinded polymorphism (type-level defunctionalization/brands).
- Macros: Procedural macros (
def_kind!,impl_kind!,Apply!) to simplify HKT boilerplate and type application. - Type Classes: A comprehensive collection of standard type classes including:
- Core:
Functor,Applicative,Monad,Semigroup,Monoid,Foldable,Traversable - Collections:
Compactable,Filterable,Witherable - Category Theory:
Category,Semigroupoid - Utilities:
Pointed,Lift,ApplyFirst,ApplySecond,Semiapplicative,Semimonad - Advanced/Internal:
MonadRec,RefFunctor,Defer,SendDefer - Function & Pointer Abstractions:
Function,CloneableFn,SendCloneableFn,ParFoldable,Pointer,RefCountedPointer,SendRefCountedPointer
- Core:
- Helper Functions: Standard FP utilities:
compose,constant,flip,identity
- Data Types: Implementations for standard and custom types:
- Standard Library:
Option,Result,Vec,String - Laziness, Memoization & Stack Safety:
Lazy,Thunk,Trampoline,Free - Generic Containers:
Identity,Pair - Function Wrappers:
Endofunction,Endomorphism,SendEndofunction - Marker Types:
RcBrand,ArcBrand,FnBrand
- Standard Library:
Motivation
Rust is a multi-paradigm language with strong functional programming features like iterators, closures, and algebraic data types. However, it lacks native support for Higher-Kinded Types (HKT), which limits the ability to write generic code that abstracts over type constructors (e.g., writing a function that works for any Monad, whether it's Option, Result, or Vec).
fp-library aims to bridge this gap by providing:
- A robust encoding of HKTs in stable Rust.
- A comprehensive set of standard type classes (
Functor,Monad,Traversable, etc.). - Zero-cost abstractions that respect Rust's performance characteristics.
Usage
Add fp-library to your Cargo.toml:
[]
= "0.8"
Crate Features
The library offers optional features that can be enabled in your Cargo.toml:
rayon: Enables parallel folding operations (ParFoldable) and parallel execution support forVecBrandusing the rayon library.
To enable this feature:
[]
= { = "0.8", = ["rayon"] }
Example: Using Functor with Option
use ;
How it Works
Higher-Kinded Types (HKT)
Since Rust doesn't support HKTs directly (i.e., it's not possible to use Option in impl Functor for Option, instead of Option<T>), this library uses Lightweight Higher-Kinded Polymorphism (also known as the "Brand" pattern or type-level defunctionalization).
Each type constructor has a corresponding Brand type (e.g., OptionBrand for Option). These brands implement the Kind traits, which map the brand and generic arguments back to the concrete type. The library provides macros to simplify this process.
use ;
;
impl_kind!
Zero-Cost Abstractions & Uncurried Semantics
Unlike many functional programming libraries that strictly adhere to curried functions (e.g., map(f)(fa)), fp-library adopts uncurried semantics (e.g., map(f, fa)) for its core abstractions.
Why? Traditional currying in Rust often requires:
- Creating intermediate closures for each partial application.
- Heap-allocating these closures (boxing) or wrapping them in reference counters (
Rc/Arc) to satisfy type system constraints. - Dynamic dispatch (
dyn Fn), which inhibits compiler optimizations like inlining.
By using uncurried functions with impl Fn or generic bounds, fp-library achieves zero-cost abstractions:
- No Heap Allocation: Operations like
mapandbinddo not allocate intermediate closures. - Static Dispatch: The compiler can fully monomorphize generic functions, enabling aggressive inlining and optimization.
- Ownership Friendly: Better integration with Rust's ownership and borrowing system.
This approach ensures that using high-level functional abstractions incurs no runtime penalty compared to hand-written imperative code.
Exceptions: While the library strives for zero-cost abstractions, some operations inherently require dynamic dispatch or heap allocation due to Rust's type system:
- Functions as Data: When functions are stored in data structures (e.g., inside a
VecforSemiapplicative::apply, or inLazythunks), they must often be "type-erased" (wrapped inRc<dyn Fn>orArc<dyn Fn>). This is because every closure in Rust has a unique, anonymous type. To store multiple different closures in the same container, or to compose functions dynamically (like inEndofunction), they must be coerced to a common trait object. - Lazy Evaluation: The
Lazytype relies on storing a thunk that can be cloned and evaluated later, which typically requires reference counting and dynamic dispatch.
For these specific cases, the library provides Brand types (like RcFnBrand and ArcFnBrand) to let you choose the appropriate wrapper (single-threaded vs. thread-safe) while keeping the rest of your code zero-cost. The library uses a unified Pointer hierarchy to abstract over these choices.
Lazy Evaluation & Effect System
Rust is an eagerly evaluated language. To enable functional patterns like deferred execution and safe recursion, fp-library provides a granular set of types that let you opt-in to specific behaviors without paying for unnecessary overhead.
| Type | Primary Use Case | Stack Safe? | Memoized? | Lifetimes? | HKT Traits |
|---|---|---|---|---|---|
Thunk<'a, A> |
Glue Code & Borrowing. Lightweight deferred computation. Best for short chains and working with references. | ⚠️ Partial (tail_rec_m only) |
❌ No | ✅ 'a |
✅ Functor, Applicative, Monad |
Trampoline<A> |
Deep Recursion & Pipelines. Heavy-duty computation. Uses a trampoline to guarantee stack safety for infinite recursion. | ✅ Yes | ❌ No | ❌ 'static |
❌ No |
Lazy<'a, A> |
Caching. Wraps a computation to ensure it runs at most once. | N/A | ✅ Yes | ✅ 'a |
✅ RefFunctor |
The "Why" of Three Types
Unlike lazy languages (e.g., Haskell) where the runtime handles everything, Rust requires us to choose our trade-offs:
ThunkvsTrampoline:Thunkis faster and supports borrowing (&'a T). Itstail_rec_mis stack-safe, but deepbindchains will overflow the stack.Trampolineguarantees stack safety for all operations via a trampoline (theFreemonad) but requires types to be'staticandSend. A key distinction is thatThunkimplementsFunctor,Applicative, andMonaddirectly, making it suitable for generic programming, whileTrampolinedoes not.- Computation vs Caching:
ThunkandTrampolinedescribe computations—they re-run every time you call.evaluate(). If you have an expensive operation (like a DB call), convert it to aLazyto cache the result.
Workflow Example: Expression Evaluator
A robust pattern is to use TryTrampoline for stack-safe, fallible recursion, TryLazy to memoize expensive results, and TryThunk to create lightweight views.
Consider an expression evaluator that handles division errors and deep recursion:
use *;
// 1. Stack-safe recursion with error handling (TryTrampoline)
// Usage
Thread Safety and Parallelism
The library supports thread-safe operations through the SendCloneableFn extension trait and parallel folding via ParFoldable.
SendCloneableFn: ExtendsCloneableFnto provideSend + Syncfunction wrappers. Implemented byArcFnBrand.ParFoldable: Providespar_fold_mapandpar_fold_rightfor parallel execution.- Rayon Support:
VecBrandsupports parallel execution usingrayonwhen therayonfeature is enabled.
use ;
let v = vec!;
// Create a thread-safe function wrapper
let f = ;
// Fold in parallel (if rayon feature is enabled)
let result = ;
assert_eq!;
Documentation
- API Documentation: The complete API reference on docs.rs.
- Architecture & Design: Details on design decisions like uncurried semantics and type parameter ordering.
- Limitations: Details all current limitations.
Contributing
We welcome contributions! Please feel free to submit a Pull Request.
Development Environment
This project uses Nix to manage the development environment.
- Install Nix Package Manager.
- Install nix-direnv (recommended) for automatic environment loading.
To set up the environment:
# If using direnv
# Or manually enter the shell
This will provide a shell with the correct Rust version and dependencies.
Project Structure
fp-library/src/classes: Contains the definitions of type classes (traits).fp-library/src/types: Contains implementations of type classes for various data types.fp-library/src/kinds: Contains the machinery for higher-kinded types.fp-library/src/brands: Contains type brands used for HKT encoding.fp-library/src/functions: Contains general helper functions.fp-macros: Procedural macros for generating HKT traits and implementations.
Release Process
For maintainers, the release process is documented in docs/release-process.md.
Benchmarking
This project uses Criterion.rs for benchmarking to ensure zero-cost abstractions and detect performance regressions.
To run all benchmarks:
To list available benchmarks:
To run a specific benchmark (e.g., Vec):
Benchmark reports are generated in target/criterion/report/index.html.
License
This project is licensed under the Blue Oak Model License 1.0.0.
References
- Lightweight higher-kinded polymorphism
- Typeclassopedia
- Lean Mathlib Prelude
- PureScript Pursuit
- Haskell base package Prelude
- PureScript Typeclass Hierarchy
- Where to find theoretical background (i.e., resources) behind PureScript classes?
- Counterexamples of Type Classes
- Haskell semigroupoids package
- Why not Pointed?
- Pluggable lifetimes
- Scala Cats