Advanced Product Types
Tutorial Video
Text description (accessibility)
This video demonstrates the "Advanced Product Types" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Product types (tuples, structs, records) combine multiple values into one. Key difference from OCaml: 1. **Tuple syntax**: Rust `(A, B)` built
Tutorial
The Problem
Product types (tuples, structs, records) combine multiple values into one. In category theory, a product A Ć B has projections fst: AĆB ā A and snd: AĆB ā B. Beyond tuples, advanced product types include heterogeneous lists (type-level lists of different types), named product types with field accessors, and type-level products enabling generic programming over record shapes. Understanding product types as mathematical objects clarifies why certain operations are natural (projection, bimap) and others require additional constraints.
🎯 Learning Outcomes
Pair<A, B> models the categorical product with projections fst and sndbimap(f, g) transforms both components independentlycurry and uncurry relate functions on pairs to two-argument functionsHNil and HConsCode Example
#![allow(clippy::all)]
//! # Advanced Product Types
//!
//! Tuples, records, and heterogeneous collections.
/// Pair type - canonical two-way product.
#[derive(Debug, Clone, PartialEq)]
pub struct Pair<A, B>(pub A, pub B);
impl<A, B> Pair<A, B> {
pub fn new(a: A, b: B) -> Self {
Pair(a, b)
}
pub fn fst(&self) -> &A {
&self.0
}
pub fn snd(&self) -> &B {
&self.1
}
pub fn swap(self) -> Pair<B, A> {
Pair(self.1, self.0)
}
pub fn map_fst<C>(self, f: impl FnOnce(A) -> C) -> Pair<C, B> {
Pair(f(self.0), self.1)
}
pub fn map_snd<C>(self, f: impl FnOnce(B) -> C) -> Pair<A, C> {
Pair(self.0, f(self.1))
}
}
/// Triple type.
#[derive(Debug, Clone, PartialEq)]
pub struct Triple<A, B, C>(pub A, pub B, pub C);
impl<A, B, C> Triple<A, B, C> {
pub fn new(a: A, b: B, c: C) -> Self {
Triple(a, b, c)
}
pub fn first(&self) -> &A {
&self.0
}
pub fn second(&self) -> &B {
&self.1
}
pub fn third(&self) -> &C {
&self.2
}
}
/// Zip two vectors into pairs.
pub fn zip<A, B>(xs: Vec<A>, ys: Vec<B>) -> Vec<Pair<A, B>> {
xs.into_iter().zip(ys).map(|(a, b)| Pair(a, b)).collect()
}
/// Unzip pairs into two vectors.
pub fn unzip<A, B>(pairs: Vec<Pair<A, B>>) -> (Vec<A>, Vec<B>) {
pairs.into_iter().map(|Pair(a, b)| (a, b)).unzip()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pair() {
let p = Pair::new(1, "hello");
assert_eq!(*p.fst(), 1);
assert_eq!(*p.snd(), "hello");
}
#[test]
fn test_swap() {
let p = Pair::new(1, 2);
assert_eq!(p.swap(), Pair(2, 1));
}
#[test]
fn test_zip_unzip() {
let xs = vec![1, 2, 3];
let ys = vec!["a", "b", "c"];
let zipped = zip(xs, ys);
assert_eq!(zipped.len(), 3);
let (xs2, ys2) = unzip(zipped);
assert_eq!(xs2, vec![1, 2, 3]);
}
}Key Differences
(A, B) built-in tuples; OCaml 'a * 'b product types ā both are built-in.HCons/HNil; OCaml uses first-class modules or GADT-based approaches for type-level heterogeneous lists.f a b is natural; Rust functions are uncurried ā curry is a conversion utility.C with functions to A and B factors through A Ć B ā this motivates the API design.OCaml Approach
type ('a, 'b) pair = { fst: 'a; snd: 'b }
let bimap f g { fst; snd } = { fst = f fst; snd = g snd }
let curry f a b = f { fst = a; snd = b }
let uncurry f { fst; snd } = f fst snd
Full Source
#![allow(clippy::all)]
//! # Advanced Product Types
//!
//! Tuples, records, and heterogeneous collections.
/// Pair type - canonical two-way product.
#[derive(Debug, Clone, PartialEq)]
pub struct Pair<A, B>(pub A, pub B);
impl<A, B> Pair<A, B> {
pub fn new(a: A, b: B) -> Self {
Pair(a, b)
}
pub fn fst(&self) -> &A {
&self.0
}
pub fn snd(&self) -> &B {
&self.1
}
pub fn swap(self) -> Pair<B, A> {
Pair(self.1, self.0)
}
pub fn map_fst<C>(self, f: impl FnOnce(A) -> C) -> Pair<C, B> {
Pair(f(self.0), self.1)
}
pub fn map_snd<C>(self, f: impl FnOnce(B) -> C) -> Pair<A, C> {
Pair(self.0, f(self.1))
}
}
/// Triple type.
#[derive(Debug, Clone, PartialEq)]
pub struct Triple<A, B, C>(pub A, pub B, pub C);
impl<A, B, C> Triple<A, B, C> {
pub fn new(a: A, b: B, c: C) -> Self {
Triple(a, b, c)
}
pub fn first(&self) -> &A {
&self.0
}
pub fn second(&self) -> &B {
&self.1
}
pub fn third(&self) -> &C {
&self.2
}
}
/// Zip two vectors into pairs.
pub fn zip<A, B>(xs: Vec<A>, ys: Vec<B>) -> Vec<Pair<A, B>> {
xs.into_iter().zip(ys).map(|(a, b)| Pair(a, b)).collect()
}
/// Unzip pairs into two vectors.
pub fn unzip<A, B>(pairs: Vec<Pair<A, B>>) -> (Vec<A>, Vec<B>) {
pairs.into_iter().map(|Pair(a, b)| (a, b)).unzip()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pair() {
let p = Pair::new(1, "hello");
assert_eq!(*p.fst(), 1);
assert_eq!(*p.snd(), "hello");
}
#[test]
fn test_swap() {
let p = Pair::new(1, 2);
assert_eq!(p.swap(), Pair(2, 1));
}
#[test]
fn test_zip_unzip() {
let xs = vec![1, 2, 3];
let ys = vec!["a", "b", "c"];
let zipped = zip(xs, ys);
assert_eq!(zipped.len(), 3);
let (xs2, ys2) = unzip(zipped);
assert_eq!(xs2, vec![1, 2, 3]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pair() {
let p = Pair::new(1, "hello");
assert_eq!(*p.fst(), 1);
assert_eq!(*p.snd(), "hello");
}
#[test]
fn test_swap() {
let p = Pair::new(1, 2);
assert_eq!(p.swap(), Pair(2, 1));
}
#[test]
fn test_zip_unzip() {
let xs = vec![1, 2, 3];
let ys = vec!["a", "b", "c"];
let zipped = zip(xs, ys);
assert_eq!(zipped.len(), 3);
let (xs2, ys2) = unzip(zipped);
assert_eq!(xs2, vec![1, 2, 3]);
}
}
Deep Comparison
Product Types
A product combines multiple values into one:
(A, B)struct Pair<A, B> { a: A, b: B }Operations
fst/snd - projectionsswap - exchange positionszip/unzip - convert listsExercises
fn zip<A, B>(a: Vec<A>, b: Vec<B>) -> Vec<Pair<A, B>> and fn unzip<A, B>(pairs: Vec<Pair<A, B>>) -> (Vec<A>, Vec<B>).fn add_curried(a: i32) -> impl Fn(i32) -> i32 as a manually curried addition, then verify uncurry(add_curried)(Pair(3, 4)) == 7.HLen with fn len() -> usize on HNil (returns 0) and HCons<H, T: HLen> (returns 1 + T::len()).