Monoid Pattern — Generic Combining
Tutorial Video
Text description (accessibility)
This video demonstrates the "Monoid Pattern — Generic Combining" functional Rust example. Difficulty level: Advanced. Key concepts covered: Type Classes, Algebraic Abstractions. Implement the monoid pattern: define a trait/module type with an identity element and an associative binary operation, then write a single generic `concat_all` function that folds any list using any monoid instance (sum, product, string concatenation, boolean AND/OR). Key difference from OCaml: 1. **Abstraction mechanism:** OCaml uses first
Tutorial
The Problem
Implement the monoid pattern: define a trait/module type with an identity element and an associative binary operation, then write a single generic concat_all function that folds any list using any monoid instance (sum, product, string concatenation, boolean AND/OR).
🎯 Learning Outcomes
Iterator::fold as the idiomatic Rust parallel to List.fold_leftSum(i64), Product(i64)) to give the same underlying type different monoid behavior🦀 The Rust Way
Rust uses a Monoid trait with empty() and combine() methods. Since Rust's trait system is type-level (not value-level), we use newtype wrappers (Sum, Product, Concat, All, Any) to distinguish different monoid behaviors for the same underlying type. The generic function concat_all<M: Monoid> uses the trait bound to dispatch statically at compile time.
Code Example
pub trait Monoid {
fn empty() -> Self;
fn combine(self, other: Self) -> Self;
}
pub fn concat_all<M: Monoid>(items: impl IntoIterator<Item = M>) -> M {
items.into_iter().fold(M::empty(), M::combine)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sum(pub i64);
impl Monoid for Sum {
fn empty() -> Self { Sum(0) }
fn combine(self, other: Self) -> Self { Sum(self.0 + other.0) }
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Product(pub i64);
impl Monoid for Product {
fn empty() -> Self { Product(1) }
fn combine(self, other: Self) -> Self { Product(self.0 * other.0) }
}Key Differences
type t = int; Rust needs newtype wrappers (Sum(i64) vs Product(i64)) because a type can only implement a trait once.List.fold_left takes a function and initial value; Rust's Iterator::fold is identical in spirit but works on any iterator, not just lists.M.empty is a module field access; Rust's M::empty() is an associated function call resolved through the trait.OCaml Approach
OCaml uses a module type MONOID signature and first-class modules. The concat_all function takes a packed module (module M : MONOID with type t = a) as a value argument, allowing the caller to pass different monoid implementations at runtime. Modules are true first-class values — you can store them in data structures, pass them to functions, and pattern-match on them.
Full Source
#![allow(clippy::all)]
/// A monoid is a type with an associative binary operation and an identity element.
///
/// This mirrors OCaml's `module type MONOID = sig type t val empty : t val combine : t -> t -> t end`
/// using Rust's trait system instead of first-class modules.
pub trait Monoid {
fn empty() -> Self;
fn combine(self, other: Self) -> Self;
}
// --- Solution 1: Idiomatic Rust — iterator fold with trait bound ---
// Uses Iterator::fold, the direct analogue of OCaml's List.fold_left
pub fn concat_all<M: Monoid>(items: impl IntoIterator<Item = M>) -> M {
items.into_iter().fold(M::empty(), M::combine)
}
// --- Solution 2: Recursive — closer to OCaml's fold_left unrolled ---
// Explicit recursion over a slice, mirroring OCaml pattern matching on lists.
// Requires Clone because we need to produce owned values from references.
pub fn concat_all_recursive<M: Monoid + Clone>(items: &[M]) -> M {
match items {
[] => M::empty(),
[x] => x.clone(),
[x, rest @ ..] => M::combine(x.clone(), concat_all_recursive(rest)),
}
}
// --- Solution 3: reduce-based — avoids calling empty() when list is non-empty ---
// Uses Iterator::reduce, returning None for empty iterators.
pub fn concat_all_reduce<M: Monoid>(items: impl IntoIterator<Item = M>) -> Option<M> {
items.into_iter().reduce(M::combine)
}
// --- Monoid instances ---
/// Additive monoid over i64 (identity: 0, operation: +)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sum(pub i64);
impl Monoid for Sum {
fn empty() -> Self {
Sum(0)
}
fn combine(self, other: Self) -> Self {
Sum(self.0 + other.0)
}
}
/// Multiplicative monoid over i64 (identity: 1, operation: *)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Product(pub i64);
impl Monoid for Product {
fn empty() -> Self {
Product(1)
}
fn combine(self, other: Self) -> Self {
Product(self.0 * other.0)
}
}
/// String concatenation monoid (identity: "", operation: +)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Concat(pub String);
impl Monoid for Concat {
fn empty() -> Self {
Concat(String::new())
}
fn combine(self, other: Self) -> Self {
Concat(self.0 + &other.0)
}
}
/// Boolean AND monoid (identity: true, operation: &&)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct All(pub bool);
impl Monoid for All {
fn empty() -> Self {
All(true)
}
fn combine(self, other: Self) -> Self {
All(self.0 && other.0)
}
}
/// Boolean OR monoid (identity: false, operation: ||)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Any(pub bool);
impl Monoid for Any {
fn empty() -> Self {
Any(false)
}
fn combine(self, other: Self) -> Self {
Any(self.0 || other.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
// --- Sum tests ---
#[test]
fn sum_empty_list() {
assert_eq!(concat_all::<Sum>(vec![]), Sum(0));
}
#[test]
fn sum_single_element() {
assert_eq!(concat_all(vec![Sum(42)]), Sum(42));
}
#[test]
fn sum_multiple_elements() {
assert_eq!(
concat_all(vec![Sum(1), Sum(2), Sum(3), Sum(4), Sum(5)]),
Sum(15)
);
}
#[test]
fn sum_with_negatives() {
assert_eq!(concat_all(vec![Sum(-1), Sum(2), Sum(-3)]), Sum(-2));
}
// --- Product tests ---
#[test]
fn product_empty_list() {
assert_eq!(concat_all::<Product>(vec![]), Product(1));
}
#[test]
fn product_single_element() {
assert_eq!(concat_all(vec![Product(7)]), Product(7));
}
#[test]
fn product_multiple_elements() {
assert_eq!(
concat_all(vec![
Product(1),
Product(2),
Product(3),
Product(4),
Product(5)
]),
Product(120)
);
}
#[test]
fn product_with_zero() {
assert_eq!(
concat_all(vec![Product(5), Product(0), Product(3)]),
Product(0)
);
}
// --- Concat tests ---
#[test]
fn concat_empty_list() {
assert_eq!(concat_all::<Concat>(vec![]), Concat(String::new()));
}
#[test]
fn concat_single_element() {
assert_eq!(
concat_all(vec![Concat("hello".into())]),
Concat("hello".into())
);
}
#[test]
fn concat_multiple_elements() {
assert_eq!(
concat_all(vec![
Concat("hello".into()),
Concat(" ".into()),
Concat("world".into())
]),
Concat("hello world".into())
);
}
// --- All (boolean AND) tests ---
#[test]
fn all_empty_list() {
assert_eq!(concat_all::<All>(vec![]), All(true));
}
#[test]
fn all_true_elements() {
assert_eq!(concat_all(vec![All(true), All(true), All(true)]), All(true));
}
#[test]
fn all_with_false() {
assert_eq!(
concat_all(vec![All(true), All(true), All(false)]),
All(false)
);
}
// --- Any (boolean OR) tests ---
#[test]
fn any_empty_list() {
assert_eq!(concat_all::<Any>(vec![]), Any(false));
}
#[test]
fn any_all_false() {
assert_eq!(concat_all(vec![Any(false), Any(false)]), Any(false));
}
#[test]
fn any_with_true() {
assert_eq!(
concat_all(vec![Any(false), Any(true), Any(false)]),
Any(true)
);
}
// --- Recursive variant tests ---
#[test]
fn recursive_sum_empty() {
assert_eq!(concat_all_recursive::<Sum>(&[]), Sum(0));
}
#[test]
fn recursive_sum_multiple() {
assert_eq!(
concat_all_recursive(&[Sum(1), Sum(2), Sum(3), Sum(4), Sum(5)]),
Sum(15)
);
}
#[test]
fn recursive_concat_multiple() {
assert_eq!(
concat_all_recursive(&[
Concat("hello".into()),
Concat(" ".into()),
Concat("world".into())
]),
Concat("hello world".into())
);
}
// --- Reduce variant tests ---
#[test]
fn reduce_empty_returns_none() {
assert_eq!(concat_all_reduce::<Sum>(vec![]), None);
}
#[test]
fn reduce_nonempty_returns_some() {
assert_eq!(
concat_all_reduce(vec![Sum(1), Sum(2), Sum(3)]),
Some(Sum(6))
);
}
// --- Monoid law tests ---
#[test]
fn sum_identity_law() {
// combine(empty, x) == x and combine(x, empty) == x
let x = Sum(42);
assert_eq!(Sum::combine(Sum::empty(), x), x);
assert_eq!(Sum::combine(x, Sum::empty()), x);
}
#[test]
fn sum_associativity_law() {
// combine(combine(a, b), c) == combine(a, combine(b, c))
let (a, b, c) = (Sum(1), Sum(2), Sum(3));
assert_eq!(
Sum::combine(Sum::combine(a, b), c),
Sum::combine(a, Sum::combine(b, c))
);
}
#[test]
fn product_identity_law() {
let x = Product(42);
assert_eq!(Product::combine(Product::empty(), x), x);
assert_eq!(Product::combine(x, Product::empty()), x);
}
#[test]
fn product_associativity_law() {
let (a, b, c) = (Product(2), Product(3), Product(4));
assert_eq!(
Product::combine(Product::combine(a, b), c),
Product::combine(a, Product::combine(b, c))
);
}
}#[cfg(test)]
mod tests {
use super::*;
// --- Sum tests ---
#[test]
fn sum_empty_list() {
assert_eq!(concat_all::<Sum>(vec![]), Sum(0));
}
#[test]
fn sum_single_element() {
assert_eq!(concat_all(vec![Sum(42)]), Sum(42));
}
#[test]
fn sum_multiple_elements() {
assert_eq!(
concat_all(vec![Sum(1), Sum(2), Sum(3), Sum(4), Sum(5)]),
Sum(15)
);
}
#[test]
fn sum_with_negatives() {
assert_eq!(concat_all(vec![Sum(-1), Sum(2), Sum(-3)]), Sum(-2));
}
// --- Product tests ---
#[test]
fn product_empty_list() {
assert_eq!(concat_all::<Product>(vec![]), Product(1));
}
#[test]
fn product_single_element() {
assert_eq!(concat_all(vec![Product(7)]), Product(7));
}
#[test]
fn product_multiple_elements() {
assert_eq!(
concat_all(vec![
Product(1),
Product(2),
Product(3),
Product(4),
Product(5)
]),
Product(120)
);
}
#[test]
fn product_with_zero() {
assert_eq!(
concat_all(vec![Product(5), Product(0), Product(3)]),
Product(0)
);
}
// --- Concat tests ---
#[test]
fn concat_empty_list() {
assert_eq!(concat_all::<Concat>(vec![]), Concat(String::new()));
}
#[test]
fn concat_single_element() {
assert_eq!(
concat_all(vec![Concat("hello".into())]),
Concat("hello".into())
);
}
#[test]
fn concat_multiple_elements() {
assert_eq!(
concat_all(vec![
Concat("hello".into()),
Concat(" ".into()),
Concat("world".into())
]),
Concat("hello world".into())
);
}
// --- All (boolean AND) tests ---
#[test]
fn all_empty_list() {
assert_eq!(concat_all::<All>(vec![]), All(true));
}
#[test]
fn all_true_elements() {
assert_eq!(concat_all(vec![All(true), All(true), All(true)]), All(true));
}
#[test]
fn all_with_false() {
assert_eq!(
concat_all(vec![All(true), All(true), All(false)]),
All(false)
);
}
// --- Any (boolean OR) tests ---
#[test]
fn any_empty_list() {
assert_eq!(concat_all::<Any>(vec![]), Any(false));
}
#[test]
fn any_all_false() {
assert_eq!(concat_all(vec![Any(false), Any(false)]), Any(false));
}
#[test]
fn any_with_true() {
assert_eq!(
concat_all(vec![Any(false), Any(true), Any(false)]),
Any(true)
);
}
// --- Recursive variant tests ---
#[test]
fn recursive_sum_empty() {
assert_eq!(concat_all_recursive::<Sum>(&[]), Sum(0));
}
#[test]
fn recursive_sum_multiple() {
assert_eq!(
concat_all_recursive(&[Sum(1), Sum(2), Sum(3), Sum(4), Sum(5)]),
Sum(15)
);
}
#[test]
fn recursive_concat_multiple() {
assert_eq!(
concat_all_recursive(&[
Concat("hello".into()),
Concat(" ".into()),
Concat("world".into())
]),
Concat("hello world".into())
);
}
// --- Reduce variant tests ---
#[test]
fn reduce_empty_returns_none() {
assert_eq!(concat_all_reduce::<Sum>(vec![]), None);
}
#[test]
fn reduce_nonempty_returns_some() {
assert_eq!(
concat_all_reduce(vec![Sum(1), Sum(2), Sum(3)]),
Some(Sum(6))
);
}
// --- Monoid law tests ---
#[test]
fn sum_identity_law() {
// combine(empty, x) == x and combine(x, empty) == x
let x = Sum(42);
assert_eq!(Sum::combine(Sum::empty(), x), x);
assert_eq!(Sum::combine(x, Sum::empty()), x);
}
#[test]
fn sum_associativity_law() {
// combine(combine(a, b), c) == combine(a, combine(b, c))
let (a, b, c) = (Sum(1), Sum(2), Sum(3));
assert_eq!(
Sum::combine(Sum::combine(a, b), c),
Sum::combine(a, Sum::combine(b, c))
);
}
#[test]
fn product_identity_law() {
let x = Product(42);
assert_eq!(Product::combine(Product::empty(), x), x);
assert_eq!(Product::combine(x, Product::empty()), x);
}
#[test]
fn product_associativity_law() {
let (a, b, c) = (Product(2), Product(3), Product(4));
assert_eq!(
Product::combine(Product::combine(a, b), c),
Product::combine(a, Product::combine(b, c))
);
}
}
Deep Comparison
OCaml vs Rust: Monoid Pattern — Generic Combining
Side-by-Side Code
OCaml
module type MONOID = sig
type t
val empty : t
val combine : t -> t -> t
end
let concat_all (type a) (module M : MONOID with type t = a) (lst : a list) =
List.fold_left M.combine M.empty lst
module Sum = struct type t = int let empty = 0 let combine = (+) end
module Product = struct type t = int let empty = 1 let combine = ( * ) end
let () =
Printf.printf "sum: %d\n" (concat_all (module Sum) [1;2;3;4;5]);
Printf.printf "product: %d\n" (concat_all (module Product) [1;2;3;4;5])
Rust (idiomatic)
pub trait Monoid {
fn empty() -> Self;
fn combine(self, other: Self) -> Self;
}
pub fn concat_all<M: Monoid>(items: impl IntoIterator<Item = M>) -> M {
items.into_iter().fold(M::empty(), M::combine)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Sum(pub i64);
impl Monoid for Sum {
fn empty() -> Self { Sum(0) }
fn combine(self, other: Self) -> Self { Sum(self.0 + other.0) }
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Product(pub i64);
impl Monoid for Product {
fn empty() -> Self { Product(1) }
fn combine(self, other: Self) -> Self { Product(self.0 * other.0) }
}
Rust (recursive)
pub fn concat_all_recursive<M: Monoid + Clone>(items: &[M]) -> M {
match items {
[] => M::empty(),
[x] => x.clone(),
[x, rest @ ..] => M::combine(x.clone(), concat_all_recursive(rest)),
}
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Module type / Trait | module type MONOID = sig type t val empty : t val combine : t -> t -> t end | trait Monoid { fn empty() -> Self; fn combine(self, other: Self) -> Self; } |
| Generic fold function | val concat_all : (module MONOID with type t = 'a) -> 'a list -> 'a | fn concat_all<M: Monoid>(items: impl IntoIterator<Item = M>) -> M |
| Identity element | M.empty (module field) | M::empty() (associated function) |
| Binary operation | M.combine (module field, curried) | M::combine (method, two args) |
| Newtype for int | Not needed — modules disambiguate | struct Sum(pub i64) / struct Product(pub i64) |
Key Insights
(module Sum) as a runtime argument. Rust's traits are type-level — M: Monoid is resolved at compile time via monomorphization. Both achieve the same abstraction, but OCaml's is more dynamic and Rust's is zero-cost.Sum and Product both with type t = int because modules are distinct values. In Rust, i64 can only implement Monoid once, so we wrap it in newtypes (Sum(i64), Product(i64)) to create distinct types with distinct trait implementations.combine = (+) is naturally curried — (+) is already int -> int -> int. Rust's combine(self, other: Self) takes two explicit arguments. The fold call adapts naturally: OCaml's List.fold_left M.combine M.empty lst becomes Rust's items.into_iter().fold(M::empty(), M::combine).concat_all takes 'a list. Rust's version takes impl IntoIterator<Item = M>, making it work with any iterator — vectors, arrays, ranges, or lazy chains — not just lists.Clone because it works with &[M] (borrowed slice) but must produce an owned M. OCaml's garbage collector handles this transparently — values are reference-counted, so "copying" is just bumping a counter.When to Use Each Style
Use idiomatic Rust (fold) when: You want the standard, efficient approach. The fold version works with any iterator, avoids Clone bounds, and is the natural Rust way to express monoidal aggregation. This is the default choice.
Use recursive Rust when: You're teaching the OCaml-to-Rust translation pattern, or when the combining logic has complex branching that doesn't map cleanly to a single fold. The recursive version also demonstrates slice pattern matching ([x, rest @ ..]), which is a powerful Rust feature for list-like processing.
Exercises
First<T> newtype monoid that always returns its left operand (identity = None) and a Last<T> that returns the right operand, and use them to find the first and last element of a list with concat_all.Endo monoid whose elements are functions T -> T and whose binary operation is composition, then use concat_all to compose a list of string transformations.Validated applicative using the monoid pattern: accumulate all validation errors (rather than short-circuiting) by combining Vec<String> error lists.