Skip to content

The comonad

A Vex program reads at one position, and it can look at other positions. Functional programming has a name for this shape: the Store comonad. The name is useful, because it gives laws, and the laws become tests.

A Store is a pair: a function from positions to values, and one position. In Vex, the space gives the records for the keys, and the focus is one key. Store has these operations:

Store Vex Meaning
extract a field name, for example "size" read at the focus
peek(k) root.of("B", "size") read at another key
seek(k) to("B") move the focus
extend(p) all() run the program p at each key
experiment(f) others(body) run at the keys that f gives for the focus

extend is the “fill down” of a spreadsheet: one formula, evaluated at each row. Cellular automata are the standard example of extend. Refer to the Game of Life.

A comonad obeys laws. Vex tests them with random spaces and random programs, through the property tests of @mark1russell7/vex-testkit:

  • AXIS.EXTEND: the item at key k of each(all, e) equals e at origin k. This is the law “extract after extend gives the program”.
  • AXIS.OTHER-TWICE: in a pair, other twice reads at the focus.
  • AXIS.OTHERS-UNION: the targets of others, plus the focus, are the targets of all.

The old Vex had one mutable focus. A program and the axis loop both changed it, so these laws did not hold. The peer axis gave the wrong values in each version. The specification shows the test status of each law.

An axis relates an origin to a list of targets. Other languages have the same idea:

Vex XPath jQuery SQL window
the focus self the element the current row
others preceding-sibling and following-sibling .siblings() EXCLUDE CURRENT ROW
all the node set the selection the whole frame

In SQL, the word “peers” means rows with equal sort keys. Thus Vex calls its axis others.

A Vex program is a tree of data. Each function over the tree is a fold: evaluate gives a value, explain gives a trace, deps gives the reads, and serialize gives JSON. A new fold does not change the IR. This is one answer to the “expression problem”: add new functions over a fixed set of kinds.

experiment: the others of the originorigin
root.from("position")
  .others((e) => e._.subtract("position"))
  .reduce("add")
step 14 of 14

Result at A: (-17, -13)

originABCD
result(-17, -13)(-1, -1)(27, -9)(-9, 23)