monadify: Functional Programming Constructs in Rust
monadify is a Rust library that provides implementations of common functional programming constructs, with a primary focus on monads and related concepts like Functors, Applicatives, and Profunctors. The goal is to offer a practical exploration of these patterns in idiomatic Rust, serving as both a learning resource and a potentially reusable library component.
Core Concepts Implemented
The library defines and implements the following core functional programming traits:
Functor: Types that can be mapped over. Providesmap(self, f: A -> B) -> F<B>.- Implemented for
Option<A>,Result<A, E>,Vec<A>,CFn<X, A>,CFnOnce<X, A>.
- Implemented for
Apply: ExtendsFunctor. Providesapply(self, f: F<A -> B>) -> F<B>for applying a wrapped function to a wrapped value.- Implemented for
Option<A>,Result<A, E>,Vec<A>.
- Implemented for
Applicative: ExtendsApply. Providespure(x: A) -> F<A>for lifting a value into the applicative context.- Implemented for
Option<A>,Result<A, E>,Vec<A>.
- Implemented for
Bind: ExtendsApply. Providesbind(self, f: A -> F<B>) -> F<B>(also known asflatMapor>>=) for sequencing operations.- Implemented for
Option<A>,Result<A, E>,Vec<A>.
- Implemented for
Monad: A marker trait that groupsApplicativeandBind.- Implemented for
Option<A>,Result<A, E>,Vec<A>.
- Implemented for
Profunctor: Bifunctors contravariant in the first argument and covariant in the second. Providesdimap(self, f: X -> A, g: B -> Y) -> P<X, Y>.- Implemented for
CFn<A, B>andCFnOnce<A, B>.
- Implemented for
Strong: ExtendsProfunctor. Providesfirstandsecondfor operating on product types (tuples).- Implemented for
CFn<A, B>.
- Implemented for
Choice: ExtendsProfunctor. Providesleftandrightfor operating on sum types (Result).- Implemented for
CFn<A, B>.
- Implemented for
The library also includes function wrappers for heap-allocated closures:
CFn<A, B>: ABox-backed, unique-ownership, non-Clonewrapper.RcFn<A, B>: AnRc-backed, shared-ownership,Clone-able sibling that unblockslift_a1::<VecKind>and enablesmdo!over function monads. Cloning is O(1).CFnOnce<A, B>: ABox-backed wrapper for once-callable closures (intentionally notClone).
The library also includes various helper functions and macros (e.g., lift2, lift_a1, fn0!, fn1!, _1, _2, view) for working with these abstractions. Optical structures like Lens and Getter (using Profunctor encoding) are also explored.
Project Goals
- To explore and understand monads and other functional patterns from a practical Rust implementation perspective.
- To create a reusable library of these structures in idiomatic Rust.
- To serve as an educational resource for learning about functional programming concepts in Rust.
Usage Example
Here's a quick example of using the Functor trait with Option (Kind-based is now the default):
use ; // Import Kind-based Functor and marker
let some_value: = Some;
// For Kind-based, Functor<A,B> is on the marker OptionKind
let mapped_value = map;
assert_eq!;
let no_value: = None;
let mapped_none = map;
assert_eq!;
And an example using Bind (often called flat_map):
use ; // Import Kind-based Bind and marker
let opt_str: = Some;
// For Kind-based, Bind<A,B> is on the marker OptionKind
// The closure takes String because OptionKind::Of<String> is Option<String>
let result = bind;
assert_eq!;
let opt_invalid_str: = Some;
let result_invalid = bind;
assert_eq!;
For more detailed examples, please refer to the documentation comments within the source code and the test files in the tests/ directory.
Do Notation
The library includes an optional do-notation feature that provides the mdo! macro, inspired by Haskell's do expressions. It lets you write monadic computations in a flat, imperative style instead of nested closures.
Enable with: --features do-notation (optional feature; zero-dependency by default)
Syntax: mdo! { Marker; pat <- expr; ...; final_expr }
- Marker must be explicit (e.g.,
OptionKind,ResultKind::<E>) — type inference is impossible - Each
pat <- expris a monadic bind;expris cloned once per bind step guard(cond)filters elements (Option/Vec only; short-circuits on failure)let binding = expr;introduces pure local bindings- Final expression is returned raw (not auto-wrapped with
pure)
Quick example:
use ;
let result: = mdo! ;
assert_eq!;
Real-world examples: See examples/ directory:
validation.rs— Validation pipelines with short-circuit on first error (Option/Result)reader_config.rs— Environment threading (ReaderT + Config); "real power" demolist_comprehension.rs— List comprehensions withguardfiltering (Vec)
Run with: cargo run --example validation --features do-notation
State monad examples
These demonstrate the StateT State monad threading state through a computation
via mdo! do-notation — each <- bind both reads and updates the implicit state:
state_stack_machine.rs— RPN / stack calculator (state = the operand stack)state_unique_id.rs— fresh-id / gensym generator (state = a counter)state_rng_lcg.rs— deterministic LCG pseudo-random generator (state = the seed)state_bank_account.rs— running-balance ledger (state = the balance)
Run any of them with (each requires the do-notation feature):
Limitations and notes:
pureis a reserved free-call head insidemdo!blocks (rewritten toMarker::pure); use::pure-qualified or.pure()method syntax to bypassCFn/CFnOnceunsupported (notClone); useRcFninstead for aClone-able, shared-ownership function monad- At most one non-
Copyexternal value permdo!nesting level (closure capture constraint)
See monadify::mdo documentation for full details.
Writer monad examples
These demonstrate the WriterT Writer monad accumulating a monoidal log through a
computation via mdo! do-notation — tell appends to the log while the result is
threaded as usual:
writer_audit_log.rs— order/payment pipeline whoseVec<String>audit trail is the Writer log (tell)writer_cost_monoid.rs— running cost accumulated via a user-definedSum(u64)monoid (your ownSemigroup/Monoidimpl)writer_listen_censor.rs— scoped, redacting logger usinglisten(capture a sub-computation's log) andcensor(rewrite the log)writer_eval_trace.rs— arithmetic evaluator accumulating aStringstep-by-step trace
Run any of them with (each requires the do-notation feature):
Except monad examples
These demonstrate the ExceptT Except monad short-circuiting on the first error
via mdo! do-notation — a failed step aborts the computation, and catch_error
can recover:
except_form_validation.rs— user-registration form validation that short-circuits on the first invalid fieldexcept_safe_calculator.rs— calculator that throws on divide-by-zero / domain errors and recovers viacatch_errorexcept_config_loader.rs— parse config string fields, unifying parse errors viawith_except_texcept_order_pipeline.rs— order pipeline (validate → reserve → charge) that aborts on the first failure
Run any of them with (each requires the do-notation feature):
Combined transformer stack example
interpreter_full_stack.rs— a tiny expression interpreter stacking all four transformers:ReaderT(variable environment) +StateT(step counter) +WriterT(pre-order trace) +ExceptT(unbound-variable / divide-by-zero errors). BecauseExceptTis the innermost layer, a thrown error discards the accumulated state and trace — the key ordering-semantics teaching point.
Run with:
Building the Project
To build the library:
Running Tests
The library includes a comprehensive test suite to verify the laws of Functor, Applicative, Monad, etc.
To run the default Kind-based tests:
This suite includes over 120 tests covering Kind-based implementations (for Option, Result, Vec, Identity, CFn, CFnOnce, ReaderT) and Profunctor laws.
To run tests for the legacy (non-HKT) implementations, use the legacy feature flag:
This suite includes over 80 tests for the legacy versions, also all passing.
Running Benchmarks
Performance benchmarks for core operations are available using criterion.rs. To run the benchmarks:
The benchmark results can be found in target/criterion/report/index.html.
Key findings from initial benchmarks:
Functor::mapandBind::bindforOptionandResultshow negligible overhead compared to native methods.Apply::apply(which involvesBox::newforCFn) has a small, consistent overhead (around 2-4 ns).Vecoperations show more overhead due to by-value semantics and heap allocations forCFnin some cases.
License
This project is licensed under the terms of the MIT License.