Alex Vakhitov

software engineering7 min read

Why I named my company Comonad

By Alex Vakhitov

I'm Alex Vakhitov, an AI and software architect based in London. In May 2024 I founded Comonad Limited, a London applied-AI company that designs AI agent harnesses, brings AI into the software development lifecycle, automates workflows and puts governance around AI in production.

If you search for the name, almost everything you find is about Haskell and category theory. That is fair: the word was there first. This post explains what a comonad is, how it relates to the monads I wrote about in Monads 101, and what the idea has to do with the work.

A monad, briefly

In Monads 101 I described a monad as a value wrapped in a context, with two operations. return puts a plain value into the context. bind takes a wrapped value and a function that produces a new wrapped value, and chains them, while the context handles the extra work: possible failure, several results, some state or I/O.

The important property is the direction of travel. A monad makes it easy to put a value into a context and to keep building on it. It does not promise that you can get a plain value back out. You cannot, in general, take the a out of an IO a, and that restriction is the point: it keeps effects where they belong, which is the theme of my post on side effects and I/O.

A comonad turns the arrows around

A comonad is the dual of a monad. In category theory "dual" means you take the definition and reverse every arrow. In Haskell, with bind written in its flipped form =<< so the two columns line up, it looks like this:

-- Monad                            -- Comonad
return :: a -> m a                  extract   :: w a -> a
join   :: m (m a) -> m a            duplicate :: w a -> w (w a)
(=<<)  :: (a -> m b) -> m a -> m b  extend    :: (w a -> b) -> w a -> w b

Read the right-hand column as a description of a value that always sits somewhere:

  • extract gives you the value at the current focus. A comonad guarantees there is always something there to take out.
  • duplicate replaces every value with the whole structure seen from that position. Each cell of a row becomes "the row, focused on this cell".
  • extend takes a function that looks at a value together with its surroundings, and runs it at every position.

The shape of the function passed to extend is the part worth remembering: w a -> b. It receives a value in its context and returns a single result. A monadic function goes the other way, a -> m b: it receives a plain value and produces something in a context. Tarmo Uustalu and Varmo Vene, in their work on dataflow programming, argued that just as monads structure computations with effects, comonads structure computations that depend on context.

Where comonads show up

Once you know the shape, you start to recognise it:

  • Non-empty lists and infinite streams. extract is the head. duplicate gives the list of all its suffixes. A moving average is an extend over a stream: each output depends on the current element and the next few. An ordinary list is not a comonad, because an empty list has nothing to extract.
  • Zippers. A zipper is a data structure with a focus: a cursor in a list, a node in a tree. Moving the focus and computing something "from here" is comonadic.
  • Store. A function from positions to values, plus a current position. Think of a spreadsheet cell whose formula reads its neighbours, or an image filter where each output pixel depends on the pixels around it.
  • Env. A value paired with an environment it can read. It is the dual of the Reader monad from Monads 101.
  • Cellular automata. Every cell's next state is a local rule applied to the cell and its neighbours. Dan Piponi's 2006 post, "Evaluating cellular automata is comonadic", is where many Haskell programmers first met the idea.

A small example: rule 30

Haskell's standard library does not include a Comonad class; the usual one lives in Edward Kmett's comonad package. To keep the example self-contained, it defines a minimal class and a zipper over an infinite row of cells.

-- Tested with GHC 9.6.6, no packages beyond base.
class Functor w => Comonad w where
  extract   :: w a -> a
  duplicate :: w a -> w (w a)
  extend    :: (w a -> b) -> w a -> w b
  extend f = fmap f . duplicate

-- An infinite row of cells with one cell in focus.
-- The left list runs outwards from the focus.
data Zipper a = Zipper [a] a [a]

instance Functor Zipper where
  fmap f (Zipper ls x rs) = Zipper (map f ls) (f x) (map f rs)

moveLeft, moveRight :: Zipper a -> Zipper a
moveLeft  (Zipper (l:ls) x rs) = Zipper ls l (x:rs)
moveLeft  z                    = z
moveRight (Zipper ls x (r:rs)) = Zipper (x:ls) r rs
moveRight z                    = z

instance Comonad Zipper where
  extract (Zipper _ x _) = x
  duplicate z = Zipper (tail (iterate moveLeft z)) z (tail (iterate moveRight z))

-- A local rule: look at the neighbours, return the cell's next state.
rule30 :: Zipper Bool -> Bool
rule30 z@(Zipper (l:_) _ (r:_)) = l /= (extract z || r)
rule30 z                        = extract z

render :: Int -> Zipper Bool -> String
render n (Zipper ls x rs) = map cell (reverse (take n ls) ++ [x] ++ take n rs)
  where cell alive = if alive then '#' else '.'

main :: IO ()
main = mapM_ (putStrLn . render 8) (take 8 (iterate (extend rule30) start))
  where start = Zipper (repeat False) True (repeat False)

The output is the first eight generations of Wolfram's rule 30:

........#........
.......###.......
......##..#......
.....##.####.....
....##..#...#....
...##.####.###...
..##..#....#..#..
.##.####..######.

rule30 only knows about one cell and its two neighbours. It says nothing about loops, indices or the edges of the row. extend does the rest: it builds every "row focused on this cell" and applies the rule to each one. Laziness keeps the infinite row manageable, because only the cells needed for the printed window are ever computed.

Like monads, comonads come with three laws, mirror images of the monad laws. Extending with extract changes nothing. Extracting after extend f gives the same result as calling f directly. And two extend steps compose the way you would expect. The laws are what make it safe to write the local rule once and let extend run it everywhere.

Where the name comes from

Comonad takes its name from the concept this post describes. There is one line of mine, quoted on the company's site, that sums up how I approach writing code with AI: "Bounded contexts and monadic composition. That's all you need to write code with AI." That line talks about monads. The company carries the name of their dual, and the dual turns out to describe a large part of the work well.

Context first, then computation

An AI agent step has the comonadic shape. A model does not act on a bare instruction. It acts on an instruction in its surroundings: the repository, the conversation so far, the results of earlier tool calls, the permissions it has been given. The output is one decision about what to do next. In types, that is w a -> b: a value in context goes in, a single result comes out.

Much of the engineering in an agent harness is about that w. Which files, records and history the model can see. What is deliberately left out. Where the edges are. A model that sees the wrong context gives a confident answer to the wrong question, and cleverness in the prompt doesn't fix that.

Monads still have their place. Once the agent has decided, its actions are effects: writing a file, calling an API, sending a message. Those need the monadic side: chaining steps, handling failure, keeping I/O at the boundary where it can be retried, logged and audited. In the terms of that quote, the bounded context decides what the agent can see and touch, and monadic composition decides how its actions chain together. The comonad is the closer description of the first half.

The same order shows up in the principle I use for changing systems that cannot go down: know the boundary, prove the new path, keep a way back. Knowing the boundary comes first. You understand the context before you compute anything in it.

Comonad the company, comonad the idea

To be clear for anyone who arrived from a search: comonad, lower case, is the category-theory concept and the Haskell type class described above. Comonad is also a company I founded in London in May 2024, which designs AI agent harnesses and helps teams run AI in production. The two share a name, but the company isn't a software library.

If you came for the idea, the rule 30 example is a good place to start experimenting: change the rule, widen the neighbourhood, or swap the zipper for a two-dimensional grid.

Get new notes by email.