A comonad is a value in context. Where a monad lets you put a value in a container (return) but not peek inside, a comonad lets you extract the focused value but not inject one from nothing. extract reads the focus, duplicate shifts attention to every position, and extend applies a local computation everywhere. Spreadsheets, cellular automata, and image kernels are all comonads: a current cell, plus the whole neighborhood.
Extract, duplicate, extend
extract (dual of return) pulls the focused value out. duplicate (dual of join) wraps the structure so that every position becomes the focus of its own copy. extend (dual of bind) takes a co-Kleisli arrow w a → b and applies it at every position. They satisfy three laws mirroring the monad laws: extract . duplicate = id, fmap extract . duplicate = id, and duplicate . duplicate = fmap duplicate . duplicate.
Scheme
; Comonad interface:; extract : w a -> a (dual of return); duplicate : w a -> w (w a) (dual of join); extend : (w a -> b) -> w a -> w b (dual of bind);; extend f = fmap f . duplicate; Comonad laws:; extract . duplicate = id; fmap extract . duplicate = id; duplicate . duplicate = fmap duplicate . duplicate; --- Stream comonad ---; A stream is an infinite list with a focus on the head.; Represented as (focus . thunk-of-rest)
(define (make-stream head tail-thunk)
(cons head tail-thunk))
(define (s-head s) (car s))
(define (s-tail s) ((cdr s)))
(define (s-take n s)
(if (<= n 0) '()
(cons (s-head s) (s-take (- n 1) (s-tail s)))))
; Build a stream from a seed
(define (s-iterate f seed)
(make-stream seed (lambda () (s-iterate f (f seed)))))
; extract = head
(define (s-extract s) (s-head s))
; duplicate: stream of streams, each shifted one position
(define (s-duplicate s)
(make-stream s (lambda () (s-duplicate (s-tail s)))))
; extend: apply a co-Kleisli arrow at every position
(define (s-extend f s)
(make-stream (f s) (lambda () (s-extend f (s-tail s)))))
; Example: moving average (window size 3)
(define nats (s-iterate (lambda (n) (+ n 1)) 1))
(define (avg3 s)
(let* ((a (s-head s))
(b (s-head (s-tail s)))
(c (s-head (s-tail (s-tail s)))))
(/ (+ a b c) 3)))
(display "nats: ") (display (s-take 6 nats)) (newline)
(display "avg3: ") (display (s-take 6 (s-extend avg3 nats))) (newline)
; Law check: extract . duplicate = id
(display "extract(duplicate(nats)) = ")
(display (s-extract (s-duplicate nats)))
; Should equal nats itself (extract gives back the original stream)
Python
# Stream comonad in Pythonclass Stream:
def __init__(self, head, tail_fn):
self.head = head
self._tail = tail_fn
def tail(self):
return self._tail()
def take(self, n):
s, out = self, []
for _ inrange(n):
out.append(s.head)
s = s.tail()
return out
def iterate(f, seed):
return Stream(seed, lambda: iterate(f, f(seed)))
def extract(s):
return s.head
def duplicate(s):
return Stream(s, lambda: duplicate(s.tail()))
def extend(f, s):
return Stream(f(s), lambda: extend(f, s.tail()))
# Moving average over 3 elements
nats = iterate(lambda n: n + 1, 1)
def avg3(s):
a = s.head
b = s.tail().head
c = s.tail().tail().head
return (a + b + c) / 3print(f"nats: {nats.take(6)}")
print(f"avg3: {extend(avg3, nats).take(6)}")
print(f"extract(duplicate(nats)).take(3) = {extract(duplicate(nats)).take(3)}")
The Store comonad
The Store comonad pairs a function s → a (a lookup table) with a current position s. Think of a spreadsheet: every cell has a value (the function maps positions to values), and one cell is focused (the current position). extract evaluates the function at the current position. duplicate creates a Store of Stores, where each position's Store is focused on that position. Store arises from the product/exponential adjunction, composed as L∘R instead of R∘L (which gives the State monad).
Scheme
; Store comonad: (lookup-fn, position); Store s a = (s -> a, s);; extract (f, s) = f(s); duplicate (f, s) = (Store f, s); i.e., a Store whose lookup returns "Store f focused at that position"; extend g (f, s) = (ฮปs'. g(f, s'), s)
(define (make-store f pos) (list f pos))
(define (store-fn st) (car st))
(define (store-pos st) (cadr st))
(define (store-extract st)
((store-fn st) (store-pos st)))
(define (store-duplicate st)
(make-store
(lambda (new-pos) (make-store (store-fn st) new-pos))
(store-pos st)))
(define (store-extend g st)
(make-store
(lambda (new-pos) (g (make-store (store-fn st) new-pos)))
(store-pos st)))
; Example: a 1D grid of squared values, focused at position 3
(define grid (make-store (lambda (i) (* i i)) 3))
(display "extract grid: ") (display (store-extract grid)) (newline)
; 9 โ the value at position 3; Co-Kleisli arrow: sum of neighbors
(define (sum-neighbors st)
(let ((f (store-fn st))
(s (store-pos st)))
(+ (f (- s 1)) (f s) (f (+ s 1)))))
(display "sum-neighbors at 3: ") (display (sum-neighbors grid)) (newline)
; 4 + 9 + 16 = 29; Extend applies sum-neighbors at every position
(define blurred (store-extend sum-neighbors grid))
(display "blurred at 3: ") (display (store-extract blurred)) (newline)
; 29
(display "blurred at 0: ")
(display ((store-fn blurred) 0)) (newline)
; 1 + 0 + 1 = 2
(display "blurred at 5: ")
(display ((store-fn blurred) 5)) (newline)
; 16 + 25 + 36 = 77
Python
# Store comonad in Pythonclass Store:
def __init__(self, fn, pos):
self.fn = fn
self.pos = pos
def extract(st):
return st.fn(st.pos)
def duplicate(st):
return Store(lambda p: Store(st.fn, p), st.pos)
def extend(g, st):
return Store(lambda p: g(Store(st.fn, p)), st.pos)
# 1D grid: position -> position squared
grid = Store(lambda i: i * i, 3)
print(f"extract grid: {extract(grid)}") # 9def sum_neighbors(st):
return st.fn(st.pos - 1) + st.fn(st.pos) + st.fn(st.pos + 1)
print(f"sum_neighbors at 3: {sum_neighbors(grid)}") # 29
blurred = extend(sum_neighbors, grid)
print(f"blurred at 3: {extract(blurred)}") # 29print(f"blurred at 0: {blurred.fn(0)}") # 2print(f"blurred at 5: {blurred.fn(5)}") # 77
Cellular automaton via extend
A cellular automaton is a co-Kleisli arrow: it reads the neighborhood (the comonadic context) and produces the next value. extend applies this rule everywhere at once. One step of the automaton is one call to extend. Iteration is composition of co-Kleisli arrows. Conway's Game of Life works the same way with a 2D Store. This extract-then-extend loop is the categorical shape of any system that reads its environment and updates: the same pattern appears in closed feedback loops where perception feeds action feeds consolidation.
Scheme
; 1D cellular automaton using Store comonad; Rule: a cell becomes 1 if exactly one neighbor is 1, else 0.; (This is elementary Rule 90 โ produces Sierpinski triangle.)
(define (make-store f pos) (list f pos))
(define (store-fn st) (car st))
(define (store-pos st) (cadr st))
(define (store-extract st)
((store-fn st) (store-pos st)))
(define (store-extend g st)
(make-store
(lambda (new-pos) (g (make-store (store-fn st) new-pos)))
(store-pos st)))
; Initial state: single 1 at position 0, 0 everywhere else
(define (initial-grid pos)
(if (= pos 0) 10))
(define automaton (make-store initial-grid 0))
; Rule 90: XOR of left and right neighbors
(define (rule90 st)
(let ((left ((store-fn st) (- (store-pos st) 1)))
(right ((store-fn st) (+ (store-pos st) 1))))
(if (= (+ left right) 1) 10)))
; Show a row: values at positions -4 to 4
(define (show-row st)
(let loop ((i -4) (acc '()))
(if (> i 4) (reverse acc)
(loop (+ i 1)
(cons ((store-fn st) i) acc)))))
; Run a few generations
(display "gen 0: ") (display (show-row automaton)) (newline)
(define gen1 (store-extend rule90 automaton))
(display "gen 1: ") (display (show-row gen1)) (newline)
(define gen2 (store-extend rule90 gen1))
(display "gen 2: ") (display (show-row gen2)) (newline)
(define gen3 (store-extend rule90 gen2))
(display "gen 3: ") (display (show-row gen3)) (newline)
(define gen4 (store-extend rule90 gen3))
(display "gen 4: ") (display (show-row gen4))
; Sierpinski triangle emerges
Python
# 1D cellular automaton (Rule 90) via Store comonadclass Store:
def __init__(self, fn, pos):
self.fn = fn
self.pos = pos
def extend(g, st):
return Store(lambda p: g(Store(st.fn, p)), st.pos)
def show_row(st, r=4):
return [st.fn(i) for i inrange(-r, r + 1)]
# Initial: single 1 at origin
automaton = Store(lambda p: 1if p == 0else0, 0)
# Rule 90: XOR of neighborsdef rule90(st):
left = st.fn(st.pos - 1)
right = st.fn(st.pos + 1)
return1if (left + right) == 1else0
state = automaton
for gen inrange(5):
row = show_row(state)
vis = "".join("โ"if c else"ยท"for c in row)
print(f"gen {gen}: {vis}")
state = extend(rule90, state)
Monad / comonad duality
Every comonad operation is a monad operation with arrows reversed. If a monad arises from an adjunction L ⊢ R as R∘L, the same adjunction gives a comonad as L∘R. The State monad (s → (a, s)) and the Store comonad ((s → a, s)) both come from the product ⊢ exponential adjunction.
Scheme
; Monad vs comonad duality;; Monad Comonad; return : a -> m a extract : w a -> a; join : m (m a) -> m a duplicate : w a -> w (w a); bind : m a -> (a -> m b) -> m b; extend : w a -> (w a -> b) -> w b;; Adjunction L โฃ R:; Monad = R โ L (State = (s ->) โ (-, s) = s -> (a, s)); Comonad = L โ R (Store = (-, s) โ (s ->) = (s -> a, s)); The Store comonad is the dual of the State monad.; Both come from the same adjunction, composed in opposite order.; State: s -> (a, s)
(define (state-return a) (lambda (s) (cons a s)))
(define (state-bind m f)
(lambda (s)
(let* ((r (m s)) (a (car r)) (s2 (cdr r)))
((f a) s2))))
; Store: (s -> a, s)
(define (store-extract st) ((car st) (cadr st)))
(define (store-extend g st)
(list (lambda (p) (g (list (car st) p))) (cadr st)))
; Same adjunction, dual operations
(display "State return 42, run at s=0: ")
(display ((state-return 42) 0)) (newline)
; (42 . 0) โ value injected, state unchanged
(display "Store extract (x->x*x, 5): ")
(display (store-extract (list (lambda (x) (* x x)) 5)))
; 25 โ value extracted from focus
Notation reference
Blog / Haskell
Scheme
Meaning
extract :: w a → a
(s-extract s) / (store-extract st)
Pull the focused value out
duplicate :: w a → w (w a)
(s-duplicate s) / (store-duplicate st)
Shift focus to every position
extend :: (w a → b) → w a → w b
(s-extend f s) / (store-extend g st)
Apply co-Kleisli arrow everywhere
Store s a = (s → a, s)
(make-store f pos)
Lookup function + current position
Stream a = Cons a (Stream a)
(make-stream head tail-thunk)
Infinite stream focused on head
w a → b
(lambda (st) ...)
Co-Kleisli arrow
Neighbors
Milewski chapters
๐ Chapter 16 โ Adjunctions (comonads arise from adjunctions composed as L∘R)
The blog post defines the Comonad typeclass in Haskell with extract, duplicate, and extend, where duplicate and extend have default implementations in terms of each other. This page implements both explicitly for clarity. The Stream comonad uses thunks for laziness, since Scheme is strict. The Store comonad is represented as a two-element list (f pos) rather than a Haskell data type. The blog post discusses Conway's Game of Life as the motivating example for a 2D Store comonad. This page uses Rule 90 (a 1D elementary cellular automaton) to keep the code short while preserving the core insight: a cellular automaton rule is a co-Kleisli arrow, and one generation is one call to extend. The lens connection (a coalgebra a → Store s a is a getter-setter pair) is mentioned in the blog post but omitted here.
Ready for the real thing? Read the blog post. Start with the Stream comonad, then see how Store models a spreadsheet.