ExamplesBy LevelBy TopicLearning Paths
774 Advanced

774-const-generics-basics — Const Generics Basics

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "774-const-generics-basics — Const Generics Basics" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Generic programming traditionally parameterizes types by other types. Key difference from OCaml: 1. **Type

Tutorial

The Problem

Generic programming traditionally parameterizes types by other types. Const generics extend this to values known at compile time — primarily integer constants. This enables Array<i32, 8> and Array<i32, 16> to be different types with their sizes embedded in the type system, eliminating runtime bounds checks and enabling stack allocation. Stabilized in Rust 1.51, const generics are used in ndarray, embedded-hal, and the standard library's [T; N] arrays.

🎯 Learning Outcomes

  • • Declare a struct with a const N: usize type parameter
  • • Implement const fn len(&self) -> usize { N } — a compile-time constant method
  • • Understand why Array<T, 8> and Array<T, 16> are different types at compile time
  • • Use [T::default(); N] for zero-initialization with const generic size
  • • See how const generics enable stack-allocated containers without heap allocation
  • Code Example

    pub struct Array<T, const N: usize> {
        data: [T; N],
    }
    
    impl<T: Default + Copy, const N: usize> Array<T, N> {
        pub fn new() -> Self {
            Array { data: [T::default(); N] }
        }
        
        pub const fn len(&self) -> usize { N }
    }
    
    let arr: Array<i32, 5> = Array::new();

    Key Differences

  • Type-level sizes: Rust's Array<T, N> encodes N in the type; OCaml arrays have runtime-only sizes.
  • Compile-time verification: Rust can reject mismatched sizes at compile time (e.g., function expecting [u8; 32] getting [u8; 16]); OCaml needs runtime checks.
  • Stack allocation: Rust's [T; N] is always stack-allocated when N is small; OCaml arrays are always heap-allocated.
  • GADTs: OCaml's GADTs can encode some size relationships at the type level, but it requires more boilerplate than Rust's const generics.
  • OCaml Approach

    OCaml has no direct equivalent of const generics. Array sizes are runtime values: Array.make n value. The Bigarray module allows specifying element kinds but not sizes in the type. OCaml 5's effect system doesn't address this gap. For fixed-size types, OCaml uses phantom type parameters with module-level constants: module Fixed8 : sig type t val size : int end = struct type t = int array let size = 8 end.

    Full Source

    #![allow(clippy::all)]
    //! # Const Generics Basics
    //!
    //! Using compile-time constant parameters.
    
    /// Array wrapper with const generic size
    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Array<T, const N: usize> {
        data: [T; N],
    }
    
    impl<T: Default + Copy, const N: usize> Array<T, N> {
        /// Create array filled with default values
        pub fn new() -> Self {
            Array {
                data: [T::default(); N],
            }
        }
    
        /// Get the length (known at compile time)
        pub const fn len(&self) -> usize {
            N
        }
    
        /// Check if empty
        pub const fn is_empty(&self) -> bool {
            N == 0
        }
    
        /// Get element by index
        pub fn get(&self, index: usize) -> Option<&T> {
            self.data.get(index)
        }
    
        /// Set element by index
        pub fn set(&mut self, index: usize, value: T) -> bool {
            if index < N {
                self.data[index] = value;
                true
            } else {
                false
            }
        }
    }
    
    impl<T: Default + Copy, const N: usize> Default for Array<T, N> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    /// Fixed-size buffer
    #[derive(Debug)]
    pub struct Buffer<const SIZE: usize> {
        data: [u8; SIZE],
        len: usize,
    }
    
    impl<const SIZE: usize> Buffer<SIZE> {
        pub const fn new() -> Self {
            Buffer {
                data: [0; SIZE],
                len: 0,
            }
        }
    
        pub const fn capacity(&self) -> usize {
            SIZE
        }
    
        pub fn len(&self) -> usize {
            self.len
        }
    
        pub fn is_empty(&self) -> bool {
            self.len == 0
        }
    
        pub fn push(&mut self, byte: u8) -> bool {
            if self.len < SIZE {
                self.data[self.len] = byte;
                self.len += 1;
                true
            } else {
                false
            }
        }
    
        pub fn as_slice(&self) -> &[u8] {
            &self.data[..self.len]
        }
    
        pub fn clear(&mut self) {
            self.len = 0;
        }
    }
    
    impl<const SIZE: usize> Default for Buffer<SIZE> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    /// Matrix with compile-time dimensions
    #[derive(Debug, Clone)]
    pub struct Matrix<T, const ROWS: usize, const COLS: usize> {
        data: [[T; COLS]; ROWS],
    }
    
    impl<T: Default + Copy, const ROWS: usize, const COLS: usize> Matrix<T, ROWS, COLS> {
        pub fn new() -> Self {
            Matrix {
                data: [[T::default(); COLS]; ROWS],
            }
        }
    
        pub const fn rows(&self) -> usize {
            ROWS
        }
    
        pub const fn cols(&self) -> usize {
            COLS
        }
    
        pub fn get(&self, row: usize, col: usize) -> Option<&T> {
            self.data.get(row).and_then(|r| r.get(col))
        }
    
        pub fn set(&mut self, row: usize, col: usize, value: T) {
            if row < ROWS && col < COLS {
                self.data[row][col] = value;
            }
        }
    }
    
    impl<T: Default + Copy, const ROWS: usize, const COLS: usize> Default for Matrix<T, ROWS, COLS> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    /// Function with const generic parameter
    pub fn repeat<const N: usize>(value: char) -> [char; N] {
        [value; N]
    }
    
    /// Sum array elements
    pub fn sum_array<const N: usize>(arr: &[i32; N]) -> i32 {
        arr.iter().sum()
    }
    
    /// Compare array lengths at compile time
    pub fn arrays_same_size<T, U, const N: usize, const M: usize>(_a: &[T; N], _b: &[U; M]) -> bool {
        N == M
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_array_basic() {
            let mut arr: Array<i32, 5> = Array::new();
            assert_eq!(arr.len(), 5);
            arr.set(0, 42);
            assert_eq!(arr.get(0), Some(&42));
        }
    
        #[test]
        fn test_buffer() {
            let mut buf: Buffer<16> = Buffer::new();
            assert_eq!(buf.capacity(), 16);
    
            buf.push(b'H');
            buf.push(b'i');
            assert_eq!(buf.as_slice(), b"Hi");
        }
    
        #[test]
        fn test_buffer_overflow() {
            let mut buf: Buffer<2> = Buffer::new();
            assert!(buf.push(b'a'));
            assert!(buf.push(b'b'));
            assert!(!buf.push(b'c')); // Full
        }
    
        #[test]
        fn test_matrix() {
            let mut m: Matrix<i32, 3, 4> = Matrix::new();
            assert_eq!(m.rows(), 3);
            assert_eq!(m.cols(), 4);
    
            m.set(1, 2, 42);
            assert_eq!(m.get(1, 2), Some(&42));
        }
    
        #[test]
        fn test_repeat() {
            let arr: [char; 5] = repeat('x');
            assert_eq!(arr, ['x', 'x', 'x', 'x', 'x']);
        }
    
        #[test]
        fn test_sum_array() {
            let arr = [1, 2, 3, 4, 5];
            assert_eq!(sum_array(&arr), 15);
        }
    
        #[test]
        fn test_same_size() {
            let a = [1, 2, 3];
            let b = ['a', 'b', 'c'];
            let c = [1.0, 2.0];
    
            assert!(arrays_same_size(&a, &b));
            assert!(!arrays_same_size(&a, &c));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_array_basic() {
            let mut arr: Array<i32, 5> = Array::new();
            assert_eq!(arr.len(), 5);
            arr.set(0, 42);
            assert_eq!(arr.get(0), Some(&42));
        }
    
        #[test]
        fn test_buffer() {
            let mut buf: Buffer<16> = Buffer::new();
            assert_eq!(buf.capacity(), 16);
    
            buf.push(b'H');
            buf.push(b'i');
            assert_eq!(buf.as_slice(), b"Hi");
        }
    
        #[test]
        fn test_buffer_overflow() {
            let mut buf: Buffer<2> = Buffer::new();
            assert!(buf.push(b'a'));
            assert!(buf.push(b'b'));
            assert!(!buf.push(b'c')); // Full
        }
    
        #[test]
        fn test_matrix() {
            let mut m: Matrix<i32, 3, 4> = Matrix::new();
            assert_eq!(m.rows(), 3);
            assert_eq!(m.cols(), 4);
    
            m.set(1, 2, 42);
            assert_eq!(m.get(1, 2), Some(&42));
        }
    
        #[test]
        fn test_repeat() {
            let arr: [char; 5] = repeat('x');
            assert_eq!(arr, ['x', 'x', 'x', 'x', 'x']);
        }
    
        #[test]
        fn test_sum_array() {
            let arr = [1, 2, 3, 4, 5];
            assert_eq!(sum_array(&arr), 15);
        }
    
        #[test]
        fn test_same_size() {
            let a = [1, 2, 3];
            let b = ['a', 'b', 'c'];
            let c = [1.0, 2.0];
    
            assert!(arrays_same_size(&a, &b));
            assert!(!arrays_same_size(&a, &c));
        }
    }

    Deep Comparison

    OCaml vs Rust: Const Generics Basics

    Array with Compile-Time Size

    Rust

    pub struct Array<T, const N: usize> {
        data: [T; N],
    }
    
    impl<T: Default + Copy, const N: usize> Array<T, N> {
        pub fn new() -> Self {
            Array { data: [T::default(); N] }
        }
        
        pub const fn len(&self) -> usize { N }
    }
    
    let arr: Array<i32, 5> = Array::new();
    

    OCaml

    OCaml doesn't have const generics. Closest is:

    (* Using Bigarray with fixed dimensions *)
    let arr = Bigarray.Array1.create Bigarray.int Bigarray.c_layout 5
    
    (* Or GADTs for type-level naturals *)
    type z
    type 'n s
    
    type ('a, 'n) vec =
      | Nil : ('a, z) vec
      | Cons : 'a * ('a, 'n) vec -> ('a, 'n s) vec
    

    Matrix with Dimensions

    Rust

    pub struct Matrix<T, const ROWS: usize, const COLS: usize> {
        data: [[T; COLS]; ROWS],
    }
    
    let m: Matrix<f64, 3, 4> = Matrix::new();
    assert_eq!(m.rows(), 3);  // Compile-time known
    

    OCaml (GADTs approach)

    (* Requires type-level naturals encoding *)
    type ('rows, 'cols) matrix = float array array
    

    Key Differences

    AspectOCamlRust
    Const paramsNot nativeconst N: usize
    Type-level numbersGADTs (complex)Native const generics
    Array sizeRuntimeCompile-time
    Zero-costNot applicableYes
    Compile errorsRuntime if wrongCompile-time

    Exercises

  • Implement zip<const N: usize>(a: Array<A, N>, b: Array<B, N>) -> Array<(A,B), N> that pairs elements — the compiler ensures both arrays have the same size.
  • Add fn concat<const A: usize, const B: usize>(left: Array<T, A>, right: Array<T, B>) -> [T; A+B] — note this requires nightly's const arithmetic; discuss the limitation.
  • Implement a Matrix<T, const ROWS: usize, const COLS: usize> with mul that only compiles when the inner dimensions match.
  • Open Source Repos