781-const-where-bounds — Const Where Bounds
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
N >= 1 at runtime in new() for a NonEmptyArray<T, N>SIZE for PowerOfTwoBuffer<SIZE> using a runtime assertwhere [(); N - 1]: compiles on nightly but not stableconst_assert! in constructors to provide early, clear error messagesCode 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
new() call, which may be distant from the invalid literal.Make(N) is analogous to Rust's NonEmptyArray<T, N>::new() — both check N at construction.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);
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Numeric constraints | Runtime only | Compile-time |
| Error timing | Program crash | Compile error |
| Zero-size arrays | Runtime check | Won't compile |
| Divisibility | Runtime assertion | Type-level |
Exercises
WindowBuffer<T, const WINDOW: usize, const STEP: usize> that asserts STEP <= WINDOW and provides a slide() method.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.where [(); N - 1]: trick in a nightly build and document the compile error that it produces for N = 0.