๐Ÿฆ€ Functional Rust

242: Store Comonad

Difficulty: 4 Level: Expert Model any "array-like" structure as a getter function plus a focus position โ€” then apply context-aware computations across every position at once.

The Problem This Solves

Writing a 1D cellular automaton (like Conway's Game of Life in 1D, or Rule 30) in Rust is straightforward but clunky. You need an array, index arithmetic, wrap-around logic, and a loop that simultaneously reads the old generation while writing the new one:
let mut next = vec![false; n];
for i in 0..n {
 let left  = cells[(i + n - 1) % n];
 let right = cells[(i + 1) % n];
 next[i] = rule(left, cells[i], right);
}
cells = next;
This works, but every algorithm that touches neighbours duplicates this pattern. The wrap-around logic is copied everywhere. And the mutation โ€” "read old, write new" โ€” is error-prone. What if you could write the rule for a single cell and have the runtime apply it to all cells simultaneously? What if "read the neighbour" was just a function call, and the wrap-around was handled once in one place? The Store comonad does exactly this. You define the entire cellular automaton as a single function `i -> bool` (given an index, return whether that cell is alive). The `extend` operation applies a neighbourhood rule to every position at once, producing a new function that describes the next generation โ€” no mutation, no index arithmetic in the rule itself. This exists to solve exactly that pain.

The Intuition

A Store is two things: 1. A getter function `S -> A`: given any position/key of type `S`, return the value `A` there 2. A current focus of type `S`: the position we're currently "at" Think of it like a spreadsheet cursor. The spreadsheet is the getter function (given any `(row, col)`, return the cell value). The cursor is your current position. You can: The power of `extend`: your function `f` receives a Store focused at each position in turn, can call `peek` to read neighbours, and returns one value. `extend` builds a new getter function that does this for every possible position โ€” lazily, without actually iterating until you ask.
Original store: getter = |i| i * i,  focus = 4
extract()   โ†’ 16          (4ยฒ = 16)
peek(3)     โ†’ 9           (3ยฒ = 9)
seek(7)     โ†’ store focused at 7

extend(|s| s.extract() + 1):
new getter = |i| (i*i) + 1
new store focus still at 4
new extract() โ†’ 17

How It Works in Rust

// Store<S, A>: a getter function (S -> A) plus a focus position S
#[derive(Clone)]
pub struct Store<S, A> {
 getter: Rc<dyn Fn(S) -> A>,  // Rc for cheap cloning
 position: S,                  // current focus
}

impl<S: Clone + 'static, A: Clone + 'static> Store<S, A> {
 pub fn new(getter: impl Fn(S) -> A + 'static, position: S) -> Self {
     Store { getter: Rc::new(getter), position }
 }

 // extract: read value at current focus
 pub fn extract(&self) -> A { (self.getter)(self.position.clone()) }

 // peek: read value at any position (without moving)
 pub fn peek(&self, s: S) -> A { (self.getter)(s) }

 // seek: create new store focused at different position
 pub fn seek(&self, s: S) -> Store<S, A> {
     Store { getter: self.getter.clone(), position: s }
 }

 // extend: build new store by applying f at every possible position
 // f receives a Store focused at that position โ€” can call extract, peek, etc.
 pub fn extend<B: Clone + 'static>(
     &self,
     f: impl Fn(&Store<S, A>) -> B + 'static,
 ) -> Store<S, B> {
     let original = self.clone();
     let f = Rc::new(f);
     Store {
         getter: Rc::new(move |s: S| {
             let focused_here = original.seek(s);  // store focused at position s
             f(&focused_here)                       // apply f with that focus
         }),
         position: self.position.clone(),
     }
 }
}

// Build a cellular automaton store from a fixed array (with wrap-around)
fn make_cell_store(cells: &[bool]) -> Store<i32, bool> {
 let cells = cells.to_vec();
 let len = cells.len() as i32;
 Store::new(
     move |i: i32| {
         let idx = ((i % len) + len) % len;  // wrap-around indexing โ€” defined ONCE
         cells[idx as usize]
     },
     0,  // start focused at cell 0
 )
}

// Rule 30: define the rule for ONE cell โ€” extend handles the rest
fn rule30_step(store: &Store<i32, bool>) -> bool {
 let i = store.pos();
 let left   = store.peek(i - 1);  // read neighbour โ€” wrap-around handled by getter
 let center = store.extract();     // read current focus
 let right  = store.peek(i + 1);  // read other neighbour
 // Rule 30: XOR-like rule
 matches!((left, center, right),
     (false, false, true) | (false, true, false) | (false, true, true) | (true, false, false)
 )
}

// Run the automaton: one extend call per generation
let mut s = make_cell_store(&initial_cells);
for _ in 0..5 {
 s = s.extend(rule30_step);  // new generation โ€” no mutation, no index arithmetic
}
The elegance: `rule30_step` doesn't know or care about wrap-around. It just calls `store.peek(i-1)` and `store.peek(i+1)`. The wrap-around is in the getter, defined once. `extend` applies the rule at every position.

What This Unlocks

Key Differences

ConceptOCamlRust
Store type`type ('s, 'a) store = Store of ('s -> 'a) * 's``struct Store<S, A> { getter: Rc<dyn Fn(S)->A>, position: S }`
Sharing getterGC references`Rc` for cheap clone in `seek` and `extend`
`extend`Builds new `store` valueBuilds new `Store` with new `Rc<dyn Fn>`
Index wrap-aroundHandled in getter, closed overSame โ€” closure captures array and length
Comonad lawEquational proofsTested: `extract(extend(f)(s)) == f(s)`
Cellular automaton`let next_gen = Store.extend rule30_step current``s = s.extend(rule30_step)`
/// Store Comonad: models a mutable "store" (array/function at an index).
///
/// Store s a = (s -> a, s)
///
///   extract: apply the getter at the current position
///   extend:  at each position s', compute f(Store(getter, s'))
///   peek:    look at any position without moving the focus
///   pos:     get the current focus position
///
/// The Store comonad is the dual of the State monad.
/// A classic application: 1D cellular automata, where each cell's
/// next state depends on its neighborhood.

/// Store<S, A>: a "getter" function `S -> A` plus a focus position `S`.
#[derive(Clone)]
pub struct Store<S, A> {
    getter: std::rc::Rc<dyn Fn(S) -> A>,
    position: S,
}

impl<S: Clone + 'static, A: Clone + 'static> Store<S, A> {
    pub fn new(getter: impl Fn(S) -> A + 'static, position: S) -> Self {
        Store {
            getter: std::rc::Rc::new(getter),
            position,
        }
    }

    /// Comonad: extract = apply the getter at the current position.
    pub fn extract(&self) -> A {
        (self.getter)(self.position.clone())
    }

    /// peek: look up any position (not just the current one).
    pub fn peek(&self, s: S) -> A {
        (self.getter)(s)
    }

    /// pos: get the current focus position.
    pub fn pos(&self) -> S {
        self.position.clone()
    }

    /// seeks: move the focus to a new position.
    pub fn seek(&self, s: S) -> Store<S, A> {
        Store {
            getter: self.getter.clone(),
            position: s,
        }
    }

    /// Comonad: extend.
    /// Builds a new Store where each position s' holds f(Store(getter, s')).
    pub fn extend<B: Clone + 'static>(
        &self,
        f: impl Fn(&Store<S, A>) -> B + 'static,
    ) -> Store<S, B> {
        let original = self.clone();
        let f = std::rc::Rc::new(f);
        Store {
            getter: std::rc::Rc::new(move |s: S| {
                let moved = original.seek(s);
                f(&moved)
            }),
            position: self.position.clone(),
        }
    }

    /// duplicate: Store s a -> Store s (Store s a)
    /// Each position now holds the Store focused there.
    pub fn duplicate(&self) -> Store<S, Store<S, A>> {
        let original = self.clone();
        Store {
            getter: std::rc::Rc::new(move |s: S| original.seek(s)),
            position: self.position.clone(),
        }
    }
}

// โ”€โ”€ 1D Cellular Automaton via Store Comonad โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

/// Build a Store from a fixed-size array with wrap-around indexing.
fn make_cell_store(cells: &[bool]) -> Store<i32, bool> {
    let cells = cells.to_vec();
    let len = cells.len() as i32;
    Store::new(
        move |i: i32| {
            let idx = ((i % len) + len) % len;
            cells[idx as usize]
        },
        0,
    )
}

/// Rule 30: one step of the cellular automaton.
/// next(i) = f(left, center, right)
fn rule30_step(store: &Store<i32, bool>) -> bool {
    let i = store.pos();
    let left = store.peek(i - 1);
    let center = store.extract();
    let right = store.peek(i + 1);
    match (left, center, right) {
        (true,  true,  true)  => false,
        (true,  true,  false) => false,
        (true,  false, true)  => false,
        (true,  false, false) => true,
        (false, true,  true)  => true,
        (false, true,  false) => true,
        (false, false, true)  => true,
        (false, false, false) => false,
    }
}

/// Extract a generation array from a Store.
fn extract_gen(store: &Store<i32, bool>, size: usize) -> Vec<bool> {
    (0..size as i32).map(|i| store.peek(i)).collect()
}

/// Print one generation.
fn print_gen(gen: &[bool]) {
    for &b in gen {
        print!("{}", if b { '#' } else { '.' });
    }
    println!();
}

fn main() {
    println!("=== Store Comonad ===\n");
    println!("Store s a = (s -> a, s)");
    println!("extract: read at current position");
    println!("extend:  transform the getter, keeping the store structure\n");

    // Basic Store operations
    let store = Store::new(|i: i32| i * i, 4_i32);
    println!("Store (square function) at position 4:");
    println!("  extract = {}", store.extract());    // 16
    println!("  peek(3) = {}", store.peek(3));      // 9
    println!("  peek(7) = {}", store.peek(7));      // 49

    // seek: move focus
    let moved = store.seek(5);
    println!("  After seek(5): extract = {}", moved.extract()); // 25

    // extend: add 1 to every position's value
    let incremented = store.extend(|s| s.extract() + 1);
    println!("\nAfter extend(+1):");
    println!("  pos=4 -> {}", incremented.peek(4)); // 17
    println!("  pos=3 -> {}", incremented.peek(3)); // 10

    // Comonad law: extract . extend f = f
    let f = |s: &Store<i32, i32>| s.extract() * 2;
    let extended = store.extend(f);
    assert_eq!(extended.extract(), f(&store), "Law 1: extract . extend f = f");
    println!("\nComonad law 1 (extract . extend f = f): {} = {} โœ“",
        extended.extract(), f(&store));

    // Cellular automaton
    println!("\n=== Rule 30 Cellular Automaton via Store Comonad ===\n");
    let size = 21_usize;
    let mut cells = vec![false; size];
    cells[size / 2] = true; // single cell in the center

    print_gen(&cells);

    // Each generation = extend rule30_step over the Store
    let mut s = make_cell_store(&cells);
    for _ in 0..5 {
        s = s.extend(rule30_step);
        print_gen(&extract_gen(&s, size));
    }

    println!("\nStore comonad models the cellular automaton elegantly:");
    println!("  extend rule30_step   applies the rule to every cell simultaneously.");
    println!("  peek(iยฑ1)            accesses neighbors without mutation.");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_extract() {
        let s = Store::new(|i: i32| i * 3, 4_i32);
        assert_eq!(s.extract(), 12);
    }

    #[test]
    fn test_peek() {
        let s = Store::new(|i: i32| i + 10, 0_i32);
        assert_eq!(s.peek(5), 15);
        assert_eq!(s.peek(-2), 8);
    }

    #[test]
    fn test_seek() {
        let s = Store::new(|i: i32| i * i, 0_i32);
        let s2 = s.seek(6);
        assert_eq!(s2.extract(), 36);
    }

    #[test]
    fn test_extend_law() {
        // extract . extend f = f
        let s = Store::new(|i: i32| i * 2, 5_i32);
        let f = |st: &Store<i32, i32>| st.extract() + 1;
        let s2 = s.extend(f);
        assert_eq!(s2.extract(), f(&s));
    }

    #[test]
    fn test_rule30_seed() {
        // Middle cell alive, rest dead โ€” check initial extract
        let cells = vec![false; 7];
        let mut c = cells.clone();
        c[3] = true;
        let s = make_cell_store(&c);
        assert_eq!(s.peek(3), true);
        assert_eq!(s.peek(2), false);
    }
}
(* Store comonad: models a mutable store.
   Store s a = (s -> a) * s
   extract: read current value
   extend: transform the getter, keeping the store *)

type ('s, 'a) store = Store of ('s -> 'a) * 's

let pos   (Store (_, s)) = s
let peek  (Store (f, _)) s = f s
let extract (Store (f, s)) = f s

let extend (Store (f, s)) g =
  Store ((fun s' -> g (Store (f, s'))), s)

let duplicate store =
  extend store (fun s -> s)

(* A 1D cellular automaton via Store comonad! *)
(* The store is: int (position) -> bool (alive/dead), with current focus *)

let make_store arr pos =
  Store ((fun i ->
    let len = Array.length arr in
    let i' = ((i mod len) + len) mod len in
    arr.(i')
  ), pos)

let rule30 store =
  let left  = peek store (pos store - 1) in
  let cur   = extract store in
  let right = peek store (pos store + 1) in
  match (left, cur, right) with
  | (true,  true,  true)  -> false
  | (true,  true,  false) -> false
  | (true,  false, true)  -> false
  | (true,  false, false) -> true
  | (false, true,  true)  -> true
  | (false, true,  false) -> true
  | (false, false, true)  -> true
  | (false, false, false) -> false

let step arr =
  let len = Array.length arr in
  let store = make_store arr 0 in
  Array.init len (fun i ->
    let s = extend store (fun _ -> ()) in
    let s2 = Store ((fun i' ->
      let (Store (f, _)) = store in f i'), i) in
    rule30 s2
  )

let print_gen arr =
  Array.iter (fun b -> print_char (if b then '#' else ' ')) arr;
  print_newline ()

let () =
  let size = 21 in
  let gen = Array.make size false in
  gen.(size / 2) <- true;
  print_gen gen;
  let g1 = step gen in print_gen g1;
  let g2 = step g1  in print_gen g2;
  Printf.printf "Store comonad: cellular automaton\n"