Monoid Pattern — Trait-Based Combining
Functional Programming
Tutorial
The Problem
Define a Monoid trait with an identity element and an associative combine operation, then implement concat_all to fold any list of monoids into a single value using the trait.
🎯 Learning Outcomes
module type MONOID) translates to Rust traitsconcat_all as a generic fold using trait bounds: works for any type that is a Monoidi32 (addition monoid) and String (concatenation monoid) share the same interfaceCode Example
#![allow(clippy::all)]
/// Monoid trait — a type with an identity element and an associative combine.
pub trait Monoid {
fn empty() -> Self;
fn combine(self, other: Self) -> Self;
}
/// Fold a list using a Monoid, starting from the identity element.
pub fn concat_all<M: Monoid>(items: impl IntoIterator<Item = M>) -> M {
items.into_iter().fold(M::empty(), M::combine)
}
#[cfg(test)]
mod tests {
use super::*;
impl Monoid for i32 {
fn empty() -> Self {
0
}
fn combine(self, other: Self) -> Self {
self + other
}
}
impl Monoid for String {
fn empty() -> Self {
String::new()
}
fn combine(self, other: Self) -> Self {
self + &other
}
}
#[test]
fn test_sum() {
assert_eq!(concat_all([1i32, 2, 3, 4, 5]), 15);
}
#[test]
fn test_empty_sum() {
assert_eq!(concat_all(std::iter::empty::<i32>()), 0);
}
#[test]
fn test_concat_strings() {
let words = ["hello".to_string(), " ".to_string(), "world".to_string()];
assert_eq!(concat_all(words), "hello world");
}
#[test]
fn test_single_element() {
assert_eq!(concat_all([42i32]), 42);
}
}Key Differences
M.empty via the module; Rust calls M::empty() as an associated function — both are monomorphized per type(module Sum); Rust infers the monoid implementation from the type of the collection elementsOCaml Approach
OCaml uses module type MONOID = sig type t; val empty : t; val combine : t -> t -> t end and a higher-kinded concat_all (type a) (module M : MONOID with type t = a). First-class modules are passed at the call site: concat_all (module Sum) [1;2;3;4;5].
Full Source
#![allow(clippy::all)]
/// Monoid trait — a type with an identity element and an associative combine.
pub trait Monoid {
fn empty() -> Self;
fn combine(self, other: Self) -> Self;
}
/// Fold a list using a Monoid, starting from the identity element.
pub fn concat_all<M: Monoid>(items: impl IntoIterator<Item = M>) -> M {
items.into_iter().fold(M::empty(), M::combine)
}
#[cfg(test)]
mod tests {
use super::*;
impl Monoid for i32 {
fn empty() -> Self {
0
}
fn combine(self, other: Self) -> Self {
self + other
}
}
impl Monoid for String {
fn empty() -> Self {
String::new()
}
fn combine(self, other: Self) -> Self {
self + &other
}
}
#[test]
fn test_sum() {
assert_eq!(concat_all([1i32, 2, 3, 4, 5]), 15);
}
#[test]
fn test_empty_sum() {
assert_eq!(concat_all(std::iter::empty::<i32>()), 0);
}
#[test]
fn test_concat_strings() {
let words = ["hello".to_string(), " ".to_string(), "world".to_string()];
assert_eq!(concat_all(words), "hello world");
}
#[test]
fn test_single_element() {
assert_eq!(concat_all([42i32]), 42);
}
}
✓ Tests
Rust test suite
#[cfg(test)]
mod tests {
use super::*;
impl Monoid for i32 {
fn empty() -> Self {
0
}
fn combine(self, other: Self) -> Self {
self + other
}
}
impl Monoid for String {
fn empty() -> Self {
String::new()
}
fn combine(self, other: Self) -> Self {
self + &other
}
}
#[test]
fn test_sum() {
assert_eq!(concat_all([1i32, 2, 3, 4, 5]), 15);
}
#[test]
fn test_empty_sum() {
assert_eq!(concat_all(std::iter::empty::<i32>()), 0);
}
#[test]
fn test_concat_strings() {
let words = ["hello".to_string(), " ".to_string(), "world".to_string()];
assert_eq!(concat_all(words), "hello world");
}
#[test]
fn test_single_element() {
assert_eq!(concat_all([42i32]), 42);
}
}
Exercises
Monoid for Vec<T> where empty() is vec![] and combine is concatenation — verify with concat_allProduct newtype over i32 (identity = 1, combine = multiplication) and fold [1, 2, 3, 4, 5] into their productmconcat as an alias for concat_all and verify that mconcat([empty]) == empty for all implementations