ExamplesBy LevelBy TopicLearning Paths
402 Intermediate

402: Index and IndexMut Traits

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "402: Index and IndexMut Traits" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Mathematical and scientific code benefits from natural indexing syntax. Key difference from OCaml: 1. **Operator syntax**: Rust uses `[]` via traits; OCaml uses `.(i)` for arrays and `.[i]` for bytes, with custom operators rarely used.

Tutorial

The Problem

Mathematical and scientific code benefits from natural indexing syntax. A matrix should support m[(row, col)], a graph adjacency structure graph[(u, v)], and a typed map inventory["sword"]. Rust's Index and IndexMut traits operator-overload the [] subscript operator for any combination of container and index type — not just integer indexing. This enables domain-specific types to feel as natural as built-in arrays while maintaining Rust's safety guarantees.

The Index trait powers Vec<T>'s v[i], HashMap's map["key"], String's s[range], and any custom data structure that benefits from subscript notation.

🎯 Learning Outcomes

  • • Understand how std::ops::Index and IndexMut operator-overload [] indexing
  • • Learn how the Output associated type determines what container[idx] returns
  • • See how tuple index types like (usize, usize) enable 2D matrix access
  • • Understand the difference between Index (immutable borrow) and IndexMut (mutable borrow)
  • • Learn how HashMap uses Index with arbitrary key types
  • Code Example

    use std::ops::{Index, IndexMut};
    
    struct Matrix {
        rows: usize,
        cols: usize,
        data: Vec<f64>,
    }
    
    impl Index<(usize, usize)> for Matrix {
        type Output = f64;
        fn index(&self, (r, c): (usize, usize)) -> &f64 {
            &self.data[r * self.cols + c]
        }
    }
    
    impl IndexMut<(usize, usize)> for Matrix {
        fn index_mut(&mut self, (r, c): (usize, usize)) -> &mut f64 {
            &mut self.data[r * self.cols + c]
        }
    }
    
    fn main() {
        let mut m = Matrix { rows: 3, cols: 3, data: vec![0.0; 9] };
        m[(1, 2)] = 42.0;  // IndexMut
        println!("m[1][2] = {}", m[(1, 2)]);  // Index
    }

    Key Differences

  • Operator syntax: Rust uses [] via traits; OCaml uses .(i) for arrays and .[i] for bytes, with custom operators rarely used.
  • Panic vs. exception: Rust's Index panics on out-of-bounds (no Result); OCaml's arr.(i) raises Invalid_argument exception.
  • Index types: Rust allows any type as index (tuples, strings, ranges); OCaml's built-in array syntax is integer-only.
  • Mutable indexing: Rust has separate IndexMut trait for lhs[i] = val; OCaml's arr.(i) <- val is a distinct syntax form.
  • OCaml Approach

    OCaml uses .(i) syntax for arrays (arr.(i) <- val), which is built into the language for 'a array. For custom data structures, OCaml defines regular functions (Matrix.get m i j) rather than operator overloading. OCaml does support custom operators via let (.![]) m (r,c) = ... syntax (infix operator definitions), making matrix access via m.![r,c] possible but uncommon.

    Full Source

    #![allow(clippy::all)]
    //! Index and IndexMut Traits
    //!
    //! Implement `[]` indexing for your own types — with any index type, not just integers.
    
    use std::collections::HashMap;
    use std::fmt;
    use std::ops::{Index, IndexMut};
    
    /// A 2D matrix with row-major storage.
    ///
    /// Supports tuple indexing: `matrix[(row, col)]`
    #[derive(Clone, Debug)]
    pub struct Matrix {
        rows: usize,
        cols: usize,
        data: Vec<f64>,
    }
    
    impl Matrix {
        /// Creates a new matrix filled with zeros.
        pub fn new(rows: usize, cols: usize) -> Self {
            Matrix {
                rows,
                cols,
                data: vec![0.0; rows * cols],
            }
        }
    
        /// Creates a matrix from existing data.
        ///
        /// # Panics
        /// Panics if `data.len() != rows * cols`
        pub fn with_data(rows: usize, cols: usize, data: Vec<f64>) -> Self {
            assert_eq!(data.len(), rows * cols, "Data size mismatch");
            Matrix { rows, cols, data }
        }
    
        /// Returns the dimensions as (rows, cols).
        pub fn dimensions(&self) -> (usize, usize) {
            (self.rows, self.cols)
        }
    
        /// Returns an iterator over all elements in row-major order.
        pub fn iter(&self) -> impl Iterator<Item = &f64> {
            self.data.iter()
        }
    }
    
    impl Index<(usize, usize)> for Matrix {
        type Output = f64;
    
        fn index(&self, (row, col): (usize, usize)) -> &f64 {
            assert!(
                row < self.rows && col < self.cols,
                "Matrix index ({}, {}) out of bounds for {}x{} matrix",
                row,
                col,
                self.rows,
                self.cols
            );
            &self.data[row * self.cols + col]
        }
    }
    
    impl IndexMut<(usize, usize)> for Matrix {
        fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut f64 {
            assert!(
                row < self.rows && col < self.cols,
                "Matrix index ({}, {}) out of bounds for {}x{} matrix",
                row,
                col,
                self.rows,
                self.cols
            );
            &mut self.data[row * self.cols + col]
        }
    }
    
    impl fmt::Display for Matrix {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            for r in 0..self.rows {
                for c in 0..self.cols {
                    write!(f, "{:8.2}", self[(r, c)])?;
                }
                writeln!(f)?;
            }
            Ok(())
        }
    }
    
    /// A configuration store indexed by string keys.
    #[derive(Default, Debug)]
    pub struct Config {
        map: HashMap<String, String>,
    }
    
    impl Config {
        /// Creates an empty configuration.
        pub fn new() -> Self {
            Config {
                map: HashMap::new(),
            }
        }
    
        /// Sets a configuration value.
        pub fn set(&mut self, key: &str, value: &str) {
            self.map.insert(key.to_string(), value.to_string());
        }
    
        /// Gets a value, returning None if not found.
        pub fn get(&self, key: &str) -> Option<&String> {
            self.map.get(key)
        }
    
        /// Checks if a key exists.
        pub fn contains(&self, key: &str) -> bool {
            self.map.contains_key(key)
        }
    }
    
    impl Index<&str> for Config {
        type Output = String;
    
        fn index(&self, key: &str) -> &String {
            self.map
                .get(key)
                .unwrap_or_else(|| panic!("Config key not found: {}", key))
        }
    }
    
    /// A sparse vector that only stores non-zero values.
    #[derive(Default, Debug)]
    pub struct SparseVec {
        data: HashMap<usize, f64>,
        len: usize,
    }
    
    impl SparseVec {
        /// Creates a new sparse vector of given length.
        pub fn new(len: usize) -> Self {
            SparseVec {
                data: HashMap::new(),
                len,
            }
        }
    
        /// Sets a value (removes if zero).
        pub fn set(&mut self, index: usize, value: f64) {
            assert!(index < self.len, "Index out of bounds");
            if value == 0.0 {
                self.data.remove(&index);
            } else {
                self.data.insert(index, value);
            }
        }
    
        /// Returns the number of non-zero elements.
        pub fn nnz(&self) -> usize {
            self.data.len()
        }
    }
    
    impl Index<usize> for SparseVec {
        type Output = f64;
    
        fn index(&self, index: usize) -> &f64 {
            assert!(index < self.len, "Index {} out of bounds", index);
            // Return reference to stored value, or static 0.0
            self.data.get(&index).unwrap_or(&0.0)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_matrix_new() {
            let m = Matrix::new(3, 4);
            assert_eq!(m.dimensions(), (3, 4));
            assert_eq!(m[(0, 0)], 0.0);
        }
    
        #[test]
        fn test_matrix_with_data() {
            let m = Matrix::with_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]);
            assert_eq!(m[(0, 0)], 1.0);
            assert_eq!(m[(0, 1)], 2.0);
            assert_eq!(m[(1, 0)], 3.0);
            assert_eq!(m[(1, 1)], 4.0);
        }
    
        #[test]
        fn test_matrix_index_mut() {
            let mut m = Matrix::new(2, 2);
            m[(0, 1)] = 5.0;
            m[(1, 0)] = 7.0;
            assert_eq!(m[(0, 1)], 5.0);
            assert_eq!(m[(1, 0)], 7.0);
        }
    
        #[test]
        #[should_panic(expected = "out of bounds")]
        fn test_matrix_out_of_bounds() {
            let m = Matrix::new(2, 2);
            let _ = m[(5, 5)];
        }
    
        #[test]
        fn test_matrix_iter() {
            let m = Matrix::with_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]);
            let sum: f64 = m.iter().sum();
            assert_eq!(sum, 10.0);
        }
    
        #[test]
        fn test_config_set_and_index() {
            let mut cfg = Config::new();
            cfg.set("host", "localhost");
            cfg.set("port", "8080");
            assert_eq!(cfg["host"], "localhost");
            assert_eq!(cfg["port"], "8080");
        }
    
        #[test]
        fn test_config_get_option() {
            let mut cfg = Config::new();
            cfg.set("key", "value");
            assert_eq!(cfg.get("key"), Some(&"value".to_string()));
            assert_eq!(cfg.get("missing"), None);
        }
    
        #[test]
        #[should_panic(expected = "not found")]
        fn test_config_missing_key() {
            let cfg = Config::new();
            let _ = cfg["nonexistent"];
        }
    
        #[test]
        fn test_config_contains() {
            let mut cfg = Config::new();
            cfg.set("exists", "yes");
            assert!(cfg.contains("exists"));
            assert!(!cfg.contains("missing"));
        }
    
        #[test]
        fn test_sparse_vec_zero_default() {
            let sv = SparseVec::new(100);
            assert_eq!(sv[0], 0.0);
            assert_eq!(sv[50], 0.0);
            assert_eq!(sv[99], 0.0);
        }
    
        #[test]
        fn test_sparse_vec_set_and_index() {
            let mut sv = SparseVec::new(100);
            sv.set(10, 5.0);
            sv.set(50, 3.0);
            assert_eq!(sv[10], 5.0);
            assert_eq!(sv[50], 3.0);
            assert_eq!(sv[0], 0.0);
            assert_eq!(sv.nnz(), 2);
        }
    
        #[test]
        fn test_sparse_vec_set_zero_removes() {
            let mut sv = SparseVec::new(10);
            sv.set(5, 10.0);
            assert_eq!(sv.nnz(), 1);
            sv.set(5, 0.0);
            assert_eq!(sv.nnz(), 0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_matrix_new() {
            let m = Matrix::new(3, 4);
            assert_eq!(m.dimensions(), (3, 4));
            assert_eq!(m[(0, 0)], 0.0);
        }
    
        #[test]
        fn test_matrix_with_data() {
            let m = Matrix::with_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]);
            assert_eq!(m[(0, 0)], 1.0);
            assert_eq!(m[(0, 1)], 2.0);
            assert_eq!(m[(1, 0)], 3.0);
            assert_eq!(m[(1, 1)], 4.0);
        }
    
        #[test]
        fn test_matrix_index_mut() {
            let mut m = Matrix::new(2, 2);
            m[(0, 1)] = 5.0;
            m[(1, 0)] = 7.0;
            assert_eq!(m[(0, 1)], 5.0);
            assert_eq!(m[(1, 0)], 7.0);
        }
    
        #[test]
        #[should_panic(expected = "out of bounds")]
        fn test_matrix_out_of_bounds() {
            let m = Matrix::new(2, 2);
            let _ = m[(5, 5)];
        }
    
        #[test]
        fn test_matrix_iter() {
            let m = Matrix::with_data(2, 2, vec![1.0, 2.0, 3.0, 4.0]);
            let sum: f64 = m.iter().sum();
            assert_eq!(sum, 10.0);
        }
    
        #[test]
        fn test_config_set_and_index() {
            let mut cfg = Config::new();
            cfg.set("host", "localhost");
            cfg.set("port", "8080");
            assert_eq!(cfg["host"], "localhost");
            assert_eq!(cfg["port"], "8080");
        }
    
        #[test]
        fn test_config_get_option() {
            let mut cfg = Config::new();
            cfg.set("key", "value");
            assert_eq!(cfg.get("key"), Some(&"value".to_string()));
            assert_eq!(cfg.get("missing"), None);
        }
    
        #[test]
        #[should_panic(expected = "not found")]
        fn test_config_missing_key() {
            let cfg = Config::new();
            let _ = cfg["nonexistent"];
        }
    
        #[test]
        fn test_config_contains() {
            let mut cfg = Config::new();
            cfg.set("exists", "yes");
            assert!(cfg.contains("exists"));
            assert!(!cfg.contains("missing"));
        }
    
        #[test]
        fn test_sparse_vec_zero_default() {
            let sv = SparseVec::new(100);
            assert_eq!(sv[0], 0.0);
            assert_eq!(sv[50], 0.0);
            assert_eq!(sv[99], 0.0);
        }
    
        #[test]
        fn test_sparse_vec_set_and_index() {
            let mut sv = SparseVec::new(100);
            sv.set(10, 5.0);
            sv.set(50, 3.0);
            assert_eq!(sv[10], 5.0);
            assert_eq!(sv[50], 3.0);
            assert_eq!(sv[0], 0.0);
            assert_eq!(sv.nnz(), 2);
        }
    
        #[test]
        fn test_sparse_vec_set_zero_removes() {
            let mut sv = SparseVec::new(10);
            sv.set(5, 10.0);
            assert_eq!(sv.nnz(), 1);
            sv.set(5, 0.0);
            assert_eq!(sv.nnz(), 0);
        }
    }

    Deep Comparison

    OCaml vs Rust: Index Trait

    Side-by-Side Code

    OCaml — Function-based access

    type 'a matrix = {
      rows: int;
      cols: int;
      data: 'a array;
    }
    
    let get m r c =
      if r >= m.rows || c >= m.cols then failwith "Out of bounds"
      else m.data.(r * m.cols + c)
    
    let set m r c v =
      if r >= m.rows || c >= m.cols then failwith "Out of bounds"
      else m.data.(r * m.cols + c) <- v
    
    let () =
      let m = { rows = 3; cols = 3; data = Array.make 9 0 } in
      set m 1 2 42;
      Printf.printf "m[1][2] = %d\n" (get m 1 2)
    

    Rust — Operator overloading via Index trait

    use std::ops::{Index, IndexMut};
    
    struct Matrix {
        rows: usize,
        cols: usize,
        data: Vec<f64>,
    }
    
    impl Index<(usize, usize)> for Matrix {
        type Output = f64;
        fn index(&self, (r, c): (usize, usize)) -> &f64 {
            &self.data[r * self.cols + c]
        }
    }
    
    impl IndexMut<(usize, usize)> for Matrix {
        fn index_mut(&mut self, (r, c): (usize, usize)) -> &mut f64 {
            &mut self.data[r * self.cols + c]
        }
    }
    
    fn main() {
        let mut m = Matrix { rows: 3, cols: 3, data: vec![0.0; 9] };
        m[(1, 2)] = 42.0;  // IndexMut
        println!("m[1][2] = {}", m[(1, 2)]);  // Index
    }
    

    Comparison Table

    AspectOCamlRust
    Syntaxget m r c / set m r c vm[(r, c)] / m[(r, c)] = v
    Index typeFixed by function signatureAny type via Index<Idx> trait
    Return typeValue (copy)Reference (&T / &mut T)
    MutabilitySeparate set functionIndexMut trait
    CustomizationWrite new functionsImplement Index for your type
    Bounds checkingManual in functionManual in index() impl

    Index Types in Rust

    Rust's Index trait is generic over the index type:

    // Index by tuple
    impl Index<(usize, usize)> for Matrix { ... }
    
    // Index by string
    impl Index<&str> for Config { ... }
    
    // Index by custom type
    struct NodeId(usize);
    impl Index<NodeId> for Graph { ... }
    
    // Index by range (slicing)
    impl Index<Range<usize>> for String { ... }
    

    OCaml uses different functions for each access pattern.


    The Reference Return

    Rust's Index returns a reference, not a value:

    impl Index<(usize, usize)> for Matrix {
        type Output = f64;
        fn index(&self, idx: (usize, usize)) -> &f64  // returns &f64, not f64
    }
    

    This enables:

  • Zero-copy access to large values
  • IndexMut for in-place modification
  • Chained indexing: matrix[(0, 0)].method()
  • OCaml's get returns a value, requiring set for modification.


    5 Takeaways

  • **[] operator works with any index type in Rust.**
  • Tuples, strings, custom IDs — whatever makes sense for your domain.

  • OCaml uses functions; Rust uses traits.
  • Both achieve the goal; Rust's approach integrates with existing syntax.

  • **Index returns references, enabling zero-copy access.**
  • Large structs inside containers aren't copied on each access.

  • **IndexMut enables write-through: m[(i,j)] = v.**
  • The mutable reference returned allows direct assignment.

  • **Use .get() for Option-returning access.**
  • Index panics on missing keys; get() returns None.

    Exercises

  • Sparse matrix: Implement SparseMatrix using HashMap<(usize, usize), f64> with Index<(usize, usize)> that returns 0.0 for missing entries and IndexMut that inserts when set to non-zero.
  • Ring buffer: Create RingBuffer<T> with a fixed capacity and implement Index<usize> that wraps around (index n maps to n % capacity). Add a push method and verify that old entries are accessible via wrap-around index.
  • Typed map: Implement TypedMap using HashMap<TypeId, Box<dyn Any>> with Index and IndexMut that accept type witnesses and return typed references, combining the type-map pattern from example 385 with operator overloading.
  • Open Source Repos