14 releases (5 breaking)
new 0.6.1 | Apr 12, 2025 |
---|---|
0.6.0 | Apr 10, 2025 |
0.5.4 | Mar 24, 2025 |
0.4.0 | Feb 26, 2025 |
0.1.0 | Feb 4, 2025 |
#153 in Data structures
498 downloads per month
1MB
11K
SLoC
Rustica
Rustica is a comprehensive functional programming library for Rust, bringing powerful abstractions from category theory and functional programming to the Rust ecosystem. It provides a rich set of type classes, data types, and utilities commonly found in functional programming languages.
Overview
Rustica enables idiomatic functional programming in Rust by providing:
- Type Classes: Core abstractions like
Functor
,Applicative
, andMonad
- Data Types: Common functional data structures like
Maybe
,Either
,Choice
, andIO
- Monad Transformers: Powerful composition with
StateT
,ReaderT
, and more - Composable APIs: Tools for function composition and transformation
- Pure Functional Style: Patterns for immutable data and explicit effect handling
- Error Handling: Functional error handling utilities that work across different types
Whether you're coming from Haskell, Scala, or other functional languages, or just want to explore functional programming in Rust, Rustica provides the tools you need.
Getting Started
Add Rustica to your Cargo.toml
:
[dependencies]
rustica = "0.6.1"
If you want to use async features, add the async
feature:
[dependencies]
rustica = { version = "0.6.1", features = ["async"] }
If you want to use persistent vector collections, add the pvec
feature:
[dependencies]
rustica = { version = "0.6.1", features = ["pvec"] }
You can combine multiple features as needed:
[dependencies]
rustica = { version = "0.6.1", features = ["full"] }
Then import the prelude to get started:
use rustica::prelude::*;
use rustica::traits::composable::Composable;
fn main() {
// Be explicit with type annotations for generic types
let value: Maybe<i32> = Maybe::just(42);
let doubled = value.fmap(|x| x * 2);
assert_eq!(doubled.unwrap(), 84);
// Example using functional composition
let add_one = |x: i32| x + 1;
let multiply_two = |x: i32| x * 2;
let composed = compose(multiply_two, add_one);
let result = composed(3); // (3 + 1) * 2 = 8
println!("Result: {}", result);
}
Features
Type Classes
Rustica implements a wide range of type classes from category theory:
-
Basic Abstractions
Functor
- For mapping over contained valuesApplicative
- For applying functions in a contextMonad
- For sequential computationsPure
- For lifting values into a contextIdentity
- For accessing values inside contextsAlternative
- For choice between computations
-
Algebraic Structures
Semigroup
- Types with an associative binary operationMonoid
- Semigroups with an identity elementFoldable
- For reducing structuresTraversable
- For structure-preserving transformations
-
Advanced Concepts
Bifunctor
- For mapping over two type parametersContravariant
- For reversing function applicationCategory
- For abstract compositionArrow
- For generalized computationComonad
- For context-aware computationsMonadError
- For error handling in monadic contexts
Data Types
Rustica provides a rich collection of functional data types:
-
Core Types
Maybe<T>
- For optional values (likeOption<T>
)Either<L, R>
- For values with two possibilitiesId<T>
- The identity monadValidated<E, T>
- For accumulating validation errorsChoice<T>
- For representing non-deterministic computations with alternatives
-
Effect Types
IO<A>
- For pure I/O operationsState<S, A>
- For stateful computations with thread-safe implementationsReader<E, A>
- For environment-based computationsWriter<W, A>
- For logging operationsCont<R, A>
- For continuation-based programmingAsyncMonad<A>
- For asynchronous operations
-
Special Purpose
- Various wrapper types (
First
,Last
,Min
,Max
, etc.)
- Various wrapper types (
-
Persistent Collections
PersistentVector<T>
- An efficient immutable vector with structural sharing and small vector optimization
-
Transformers
StateT<S, M, A>
- State monad transformer for combining state with other effectsReaderT<E, M, A>
- Reader monad transformer for combining environment with other effectsWriterT<W, M, A>
- Writer monad transformer for combining logging with other effects- Bidirectional conversion between monads and their transformer versions
-
Optics
Lens
- For focusing on parts of structuresPrism
- For working with sum types
Error Handling Utilities
Rustica provides standardized error handling utilities that work across different functional types:
-
Core Functions
sequence
- Combines a collection ofResult
values into a singleResult
containing a collectiontraverse
- Applies a function that produces aResult
to a collection, returning a singleResult
traverse_validated
- Liketraverse
but collects all errors instead of failing fast
-
Type Conversion
ResultExt
trait - ExtendsResult
with methods liketo_validated()
andto_either()
WithError
trait - Generic trait for any type that can represent error states- Conversion functions between
Result
,Either
, andValidated
-
Error Types
AppError<M, C>
- A structured error type that provides both a message and optional context- Helper functions like
error()
anderror_with_context()
use rustica::utils::error_utils::{traverse, sequence, ResultExt};
use rustica::datatypes::validated::Validated;
// Apply a fallible function to each element, collecting into a Result
let inputs = vec!["1", "2", "3"];
let parse_int = |s: &str| s.parse::<i32>().map_err(|_| "parse error");
let results = traverse(inputs, parse_int)?; // Results in Ok(vec![1, 2, 3])
// Combine multiple Results into one
let results_vec = vec![Ok(1), Ok(2), Ok(3)];
let combined = sequence(results_vec)?; // Results in Ok(vec![1, 2, 3])
// Convert a Result to Validated for error accumulation
let result: Result<i32, &str> = Err("Input error");
let validated: Validated<&str, i32> = result.to_validated();
Higher-Kinded Types
Rustica implements a pattern for working with higher-kinded types in Rust, providing:
- The
HKT
trait for type constructors - The
BinaryHKT
trait for types with two parameters - Utilities for working with HKTs
Persistent Collections
Rustica provides persistent data structures that enable efficient immutable programming:
use rustica::prelude::*;
use rustica::pvec::PersistentVector;
use rustica::pvec; // Import the pvec! macro
// Create using the constructor
let vector = PersistentVector::<i32>::new();
let vector = vector.push_back(1).push_back(2).push_back(3);
// Create using the convenient macro
let vector = pvec![1, 2, 3, 4, 5];
// Access elements
assert_eq!(vector.get(2), Some(&3));
// Modify without changing the original
let updated = vector.update(2, 10);
assert_eq!(updated.get(2), Some(&10));
assert_eq!(vector.get(2), Some(&3)); // Original unchanged
// Small vector optimization for better performance
// Vectors with 8 or fewer elements use an optimized inline representation
let small_vec = pvec![1, 2, 3]; // Uses optimized storage
The PersistentVector
provides:
- Immutability: All operations create new versions without modifying the original
- Structural Sharing: Efficient memory usage by sharing common structure between versions
- Thread Safety: Safe to use across threads due to its immutable nature
- Memory Optimization: Special representation for small vectors to reduce overhead
This makes it ideal for functional programming patterns, concurrent applications, and scenarios where you need to maintain multiple versions of a collection efficiently.
Monad Transformers
Monad transformers let you combine the effects of multiple monads:
use rustica::prelude::*;
use rustica::transformers::StateT;
use rustica::datatypes::maybe::Maybe;
// Define a stateful computation that returns a Maybe
let state_t = StateT::new(|s: i32| {
// Increment state and return maybe value
if s > 0 {
Maybe::just((s + 1, s * 2))
} else {
Maybe::nothing()
}
});
// Run the computation with initial state
let result = state_t.run_state(5);
assert_eq!(result, Maybe::just((6, 10)));
Examples
Working with Maybe (Option)
use rustica::prelude::*;
use rustica::datatypes::maybe::Maybe;
use rustica::traits::functor::Functor;
// Using Maybe for optional values with explicit type annotations
let input = "42";
let maybe_int: Maybe<i32> = input.parse::<i32>().ok().into(); // Convert to Maybe
let result = maybe_int
.bind(|x: i32| if x > 0 { Maybe::just(x) } else { Maybe::nothing() })
.fmap(|x: i32| x * 2);
assert_eq!(result, Maybe::just(84));
Working with Choice for non-deterministic computations
use rustica::prelude::*;
use rustica::datatypes::choice::Choice;
use rustica::traits::functor::Functor;
use rustica::traits::applicative::Applicative;
// Create a Choice with a primary value and alternatives
let numbers: Choice<i32> = Choice::new(2, vec![3, 4, 5]);
// Map over all possible values
let doubled: Choice<i32> = numbers.fmap(|x| x * 2);
assert_eq!(*doubled.first().unwrap(), 4); // Primary value is 2*2=4
assert_eq!(doubled.alternatives(), &[6, 8, 10]); // Alternatives are [3*2, 4*2, 5*2]
// Apply functions with applicative
let add_one = |x: &i32| x + 1;
let multiply_by_three = |x: &i32| x * 3;
let functions = Choice::new(add_one, vec![multiply_by_three]);
let results = numbers.apply(&functions);
// Primary result is add_one(2) = 3
// Alternatives include all combinations of functions and values
Error Handling with Validated
use rustica::prelude::*;
use rustica::utils::error_utils::traverse_validated;
use rustica::datatypes::validated::Validated;
// Define validation functions
let validate_positive = |x: i32| -> Result<i32, String> {
if x > 0 {
Ok(x)
} else {
Err(format!("Value must be positive: {}", x))
}
};
let validate_even = |x: i32| -> Result<i32, String> {
if x % 2 == 0 {
Ok(x)
} else {
Err(format!("Value must be even: {}", x))
}
};
// Validate multiple values, collecting all errors
let inputs = vec![10, -5, 7, 8];
let validation_result = traverse_validated(inputs, |x| {
let x = validate_positive(x)?;
validate_even(x)
});
// Check if validation passed or inspect all errors
match validation_result {
Validated::Valid(values) => println!("All values valid: {:?}", values),
_ => {
println!("Validation errors:");
for error in validation_result.errors() {
println!(" - {}", error);
}
}
}
Working with State Monad
use rustica::prelude::*;
use rustica::datatypes::state::State;
use rustica::datatypes::state::{get, put, modify};
// Simple counter with State monad
let counter = State::new(|s: i32| (s, s + 1));
assert_eq!(counter.run_state(5), (5, 6));
// Complex state transformations with bind
let computation = get::<i32>().bind(|value| {
if value > 10 {
put(value * 2)
} else {
modify(|s: i32| s + 5)
}
});
// With initial state 5: get returns 5, then we modify to 5+5=10
assert_eq!(computation.exec_state(5), 10);
// With initial state 15: get returns 15, then we put 15*2=30
assert_eq!(computation.exec_state(15), 30);
// Using StateT for combining state with other effects
use rustica::transformers::StateT;
// StateT with Option as the base monad
let safe_counter: StateT<i32, Option<(i32, i32)>, i32> = StateT::new(|s: i32| {
if s >= 0 {
Some((s, s + 1))
} else {
None // Computation fails for negative numbers
}
});
assert_eq!(safe_counter.run_state(5), Some((5, 6)));
assert_eq!(safe_counter.run_state(-1), None);
Inspiration
Rustica is inspired by functional programming libraries in other languages:
- Haskell's standard library
- Scala's Cats
- Kotlin's Arrow
- TypeScript's fp-ts
Contributing
Contributions are welcome! Check the TODO list for areas that need work.
License
Rustica is licensed under the Apache License, version 2.0. See the LICENSE file for details.
Documentation
For detailed documentation, please visit docs.rs/rustica
Dependencies
~1.5–8MB
~63K SLoC