ExamplesBy LevelBy TopicLearning Paths
781 Fundamental

781-const-where-bounds — Const Where Bounds

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "781-const-where-bounds — Const Where Bounds" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Const generics can accept any `usize` value, but many types have validity constraints: a buffer must be non-empty, a size must be a power of two, a dimension must be positive. Key difference from OCaml: 1. **Compile vs runtime**: Nightly Rust can enforce const bounds at compile time; stable Rust and OCaml both use runtime assertions in constructors.

Tutorial

The Problem

Const generics can accept any usize value, but many types have validity constraints: a buffer must be non-empty, a size must be a power of two, a dimension must be positive. On stable Rust, these constraints are enforced via runtime assertions in new(). On nightly, where [(); N - 1]: Sized and similar tricks enforce constraints at compile time. This example shows both approaches and explains why the nightly technique is not yet stable.

🎯 Learning Outcomes

  • • Enforce N >= 1 at runtime in new() for a NonEmptyArray<T, N>
  • • Enforce power-of-two SIZE for PowerOfTwoBuffer<SIZE> using a runtime assert
  • • Understand why where [(); N - 1]: compiles on nightly but not stable
  • • Use const_assert! in constructors to provide early, clear error messages
  • • See how these patterns are used in embedded HAL crates for register sizes
  • Code Example

    pub struct NonEmptyArray<T, const N: usize>
    where
        [(); N - 1]: Sized, // Compile error if N == 0
    {
        data: [T; N],
    }
    
    // Compiles:
    let ok: NonEmptyArray<i32, 5> = NonEmptyArray::new();
    
    // Won't compile:
    // let bad: NonEmptyArray<i32, 0> = NonEmptyArray::new();

    Key Differences

  • Compile vs runtime: Nightly Rust can enforce const bounds at compile time; stable Rust and OCaml both use runtime assertions in constructors.
  • Error location: Rust's compile-time bounds produce errors at the point of type instantiation; runtime asserts fail at new() call, which may be distant from the invalid literal.
  • Functor approach: OCaml's functor + module Make(N) is analogous to Rust's NonEmptyArray<T, N>::new() — both check N at construction.
  • Stability: Rust's nightly const-bound mechanism (where [(); EXPR]:) is unstable; use runtime asserts in production code.
  • OCaml Approach

    OCaml enforces constraints at the module functor level: module Make(N: sig val n: int end) : sig ... end = struct let () = assert (N.n >= 1) ... end. This makes the assertion happen at module creation time, similar to Rust's new() assert. GADTs allow type-level encoding of some constraints: type 'n positive = Positive : positive_int -> positive positive using phantom types.

    Full Source

    #![allow(clippy::all)]
    //! # Const Where Bounds
    //!
    //! Constraining const generic parameters.
    //!
    //! Note: Rust stable doesn't support `where [(); expr]:` bounds on const generics.
    //! We demonstrate the concept using runtime assertions and trait-based patterns.
    
    /// Non-empty array — uses a const generic N and stores [T; N].
    /// The constraint N >= 1 is enforced at construction time.
    pub struct NonEmptyArray<T, const N: usize> {
        data: [T; N],
    }
    
    impl<T: Default + Copy, const N: usize> NonEmptyArray<T, N> {
        /// Panics if N == 0.
        pub fn new() -> Self {
            assert!(N >= 1, "NonEmptyArray requires N >= 1");
            NonEmptyArray {
                data: [T::default(); N],
            }
        }
    
        pub fn first(&self) -> &T {
            &self.data[0]
        }
    
        pub fn last(&self) -> &T {
            &self.data[N - 1]
        }
    }
    
    impl<T: Default + Copy, const N: usize> Default for NonEmptyArray<T, N> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    /// Power of two buffer — fast modulo via bitmask.
    /// The power-of-two constraint is checked at construction.
    pub struct PowerOfTwoBuffer<const SIZE: usize> {
        data: [u8; SIZE],
    }
    
    impl<const SIZE: usize> PowerOfTwoBuffer<SIZE> {
        pub fn new() -> Self {
            assert!(
                SIZE > 0 && (SIZE & (SIZE - 1)) == 0,
                "SIZE must be a power of 2"
            );
            PowerOfTwoBuffer { data: [0; SIZE] }
        }
    
        pub const fn size(&self) -> usize {
            SIZE
        }
    
        /// Fast modulo using bit mask (works because SIZE is power of 2).
        pub const fn wrap_index(&self, idx: usize) -> usize {
            idx & (SIZE - 1)
        }
    }
    
    /// Aligned chunks: divide M items into chunks of N.
    /// Constraint M % N == 0 is enforced at construction.
    pub struct AlignedChunks<T> {
        data: Vec<Vec<T>>,
        chunk_size: usize,
    }
    
    impl<T: Default + Clone> AlignedChunks<T> {
        pub fn new(chunk_size: usize, total: usize) -> Self {
            assert!(chunk_size > 0, "chunk_size must be > 0");
            assert!(
                total % chunk_size == 0,
                "total must be divisible by chunk_size"
            );
            let num_chunks = total / chunk_size;
            let data = vec![vec![T::default(); chunk_size]; num_chunks];
            AlignedChunks { data, chunk_size }
        }
    
        pub fn chunk_size(&self) -> usize {
            self.chunk_size
        }
    
        pub fn num_chunks(&self) -> usize {
            self.data.len()
        }
    
        pub fn get_chunk(&self, idx: usize) -> Option<&[T]> {
            self.data.get(idx).map(|v| v.as_slice())
        }
    }
    
    /// Minimum size buffer — ensures N >= 64.
    pub struct MinSizeBuffer<const N: usize> {
        data: [u8; N],
    }
    
    impl<const N: usize> MinSizeBuffer<N> {
        pub fn new() -> Self {
            assert!(N >= 64, "MinSizeBuffer requires N >= 64");
            MinSizeBuffer { data: [0; N] }
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_non_empty_array() {
            let arr: NonEmptyArray<i32, 5> = NonEmptyArray::new();
            assert_eq!(*arr.first(), 0);
            assert_eq!(*arr.last(), 0);
        }
    
        // NonEmptyArray::<i32, 0>::new() would panic at runtime
    
        #[test]
        fn test_power_of_two_buffer() {
            let buf: PowerOfTwoBuffer<16> = PowerOfTwoBuffer::new();
            assert_eq!(buf.size(), 16);
            assert_eq!(buf.wrap_index(17), 1); // 17 % 16 = 1
        }
    
        // PowerOfTwoBuffer::<15>::new() would panic (15 is not power of 2)
    
        #[test]
        fn test_aligned_chunks() {
            let chunks: AlignedChunks<i32> = AlignedChunks::new(4, 12);
            assert_eq!(chunks.chunk_size(), 4);
            assert_eq!(chunks.num_chunks(), 3);
        }
    
        // AlignedChunks::new(5, 12) would panic (12 % 5 != 0)
    
        #[test]
        fn test_min_size() {
            let buf: MinSizeBuffer<128> = MinSizeBuffer::new();
            assert_eq!(buf.data.len(), 128);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_non_empty_array() {
            let arr: NonEmptyArray<i32, 5> = NonEmptyArray::new();
            assert_eq!(*arr.first(), 0);
            assert_eq!(*arr.last(), 0);
        }
    
        // NonEmptyArray::<i32, 0>::new() would panic at runtime
    
        #[test]
        fn test_power_of_two_buffer() {
            let buf: PowerOfTwoBuffer<16> = PowerOfTwoBuffer::new();
            assert_eq!(buf.size(), 16);
            assert_eq!(buf.wrap_index(17), 1); // 17 % 16 = 1
        }
    
        // PowerOfTwoBuffer::<15>::new() would panic (15 is not power of 2)
    
        #[test]
        fn test_aligned_chunks() {
            let chunks: AlignedChunks<i32> = AlignedChunks::new(4, 12);
            assert_eq!(chunks.chunk_size(), 4);
            assert_eq!(chunks.num_chunks(), 3);
        }
    
        // AlignedChunks::new(5, 12) would panic (12 % 5 != 0)
    
        #[test]
        fn test_min_size() {
            let buf: MinSizeBuffer<128> = MinSizeBuffer::new();
            assert_eq!(buf.data.len(), 128);
        }
    }

    Deep Comparison

    OCaml vs Rust: Const Where Bounds

    Compile-Time Constraints

    Rust

    pub struct NonEmptyArray<T, const N: usize>
    where
        [(); N - 1]: Sized, // Compile error if N == 0
    {
        data: [T; N],
    }
    
    // Compiles:
    let ok: NonEmptyArray<i32, 5> = NonEmptyArray::new();
    
    // Won't compile:
    // let bad: NonEmptyArray<i32, 0> = NonEmptyArray::new();
    

    OCaml

    No compile-time numeric constraints:

    (* Runtime check only *)
    let create_non_empty n =
      if n <= 0 then invalid_arg "must be positive";
      Array.make n default
    

    Power of Two Constraint

    Rust

    pub struct PowerOfTwoBuffer<const SIZE: usize>
    where
        [(); (SIZE & (SIZE - 1))]: Sized, // Fails if not power of 2
    {
        data: [u8; SIZE],
    }
    
    // Fast wrap using bit mask
    pub const fn wrap_index(&self, idx: usize) -> usize {
        idx & (SIZE - 1)  // Same as idx % SIZE but faster
    }
    

    Key Differences

    AspectOCamlRust
    Numeric constraintsRuntime onlyCompile-time
    Error timingProgram crashCompile error
    Zero-size arraysRuntime checkWon't compile
    DivisibilityRuntime assertionType-level

    Exercises

  • Implement WindowBuffer<T, const WINDOW: usize, const STEP: usize> that asserts STEP <= WINDOW and provides a slide() method.
  • Add a MinMaxBuffer<T, const MIN_CAP: usize, const MAX_CAP: usize> that asserts MIN_CAP <= MAX_CAP and stores between MIN_CAP and MAX_CAP elements.
  • Experiment with the nightly where [(); N - 1]: trick in a nightly build and document the compile error that it produces for N = 0.
  • Open Source Repos