774-const-generics-basics — Const Generics Basics
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
const N: usize type parameterconst fn len(&self) -> usize { N } — a compile-time constant methodArray<T, 8> and Array<T, 16> are different types at compile time[T::default(); N] for zero-initialization with const generic sizeCode 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
Array<T, N> encodes N in the type; OCaml arrays have runtime-only sizes.[u8; 32] getting [u8; 16]); OCaml needs runtime checks.[T; N] is always stack-allocated when N is small; OCaml arrays are always heap-allocated.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));
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| Const params | Not native | const N: usize |
| Type-level numbers | GADTs (complex) | Native const generics |
| Array size | Runtime | Compile-time |
| Zero-cost | Not applicable | Yes |
| Compile errors | Runtime if wrong | Compile-time |
Exercises
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.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.Matrix<T, const ROWS: usize, const COLS: usize> with mul that only compiles when the inner dimensions match.