ExamplesBy LevelBy TopicLearning Paths
602 Fundamental

Advanced Product Types

Functional Programming

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

  • • How Pair<A, B> models the categorical product with projections fst and snd
  • • How bimap(f, g) transforms both components independently
  • • How curry and uncurry relate functions on pairs to two-argument functions
  • • How heterogeneous tuples encode type-level lists in Rust with HNil and HCons
  • • Where product types appear: configuration structs, multiple return values, type-level programming
  • Code 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

  • Tuple syntax: Rust (A, B) built-in tuples; OCaml 'a * 'b product types — both are built-in.
  • HList: Rust's type-level HList via HCons/HNil; OCaml uses first-class modules or GADT-based approaches for type-level heterogeneous lists.
  • Currying: OCaml functions are curried by default — f a b is natural; Rust functions are uncurried — curry is a conversion utility.
  • Category theory: The product type satisfies the universal property: any type 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]);
        }
    }
    ✓ Tests Rust test suite
    #[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:

  • • Tuples: (A, B)
  • • Structs: struct Pair<A, B> { a: A, b: B }
  • Operations

  • • fst/snd - projections
  • • swap - exchange positions
  • • zip/unzip - convert lists
  • Exercises

  • Zip pairs: Implement 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>).
  • Currying: Write fn add_curried(a: i32) -> impl Fn(i32) -> i32 as a manually curried addition, then verify uncurry(add_curried)(Pair(3, 4)) == 7.
  • HList length: Implement a trait HLen with fn len() -> usize on HNil (returns 0) and HCons<H, T: HLen> (returns 1 + T::len()).
  • Open Source Repos