Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Colimits and pushouts

Disclaimer. The content of this page is largely LM-generated. It was written as a stopgap to make the panproto system legible while we work through the book verifying and editing the content by hand. When a chapter has been verified or edited by a human, the parts that were verified or edited will be noted at the head of the chapter.

The coproduct of the previous chapter combined two objects without asking anything about how they might already be related. In the setting of panproto, and indeed in most settings where structured things have to be combined, that is not what one usually wants. Two schemas that both descend from a shared ancestor should combine in a way that identifies the shared piece; two protocols with a common sub-vocabulary should combine in a way that identifies the common vocabulary. The construction that handles this is the pushout, and the general theory it sits inside is that of colimits.

The chapter has three tasks. The first is to define a diagram — the mathematical gadget for naming a shape of combination — and the general notion of colimit over a diagram of any shape. The second is to specialise to the three-object diagram called a span, whose colimit is the pushout. The third is to show the pushout at work in panproto, where it governs both protocol composition and the three-way merge operation at the heart of schematic version control.

Diagrams

Before defining the general colimit we need a vocabulary for “shape of combination”. The relevant gadget is straightforward once one has functors in hand: a shape is itself a small category, and a diagram of that shape in the target is a functor from the shape into the target.

A diagram in a category is a functor from some small category , called the shape of the diagram, into . The small category encodes the pattern of things to be combined and the relations among them; the functor picks out specific objects and morphisms of that realise the pattern.

A reader who has survived the last two chapters may feel the definition is understated — a diagram is just a functor, which is already a familiar object. That is the force of the definition. Everything we know about functors applies to diagrams, including the whole apparatus of natural transformations and morphisms between diagrams, and the theory of colimits inherits the universal-property style of argument from the previous chapter without new machinery.

Two examples of shape categories will carry most of the weight.

The discrete shape on two objects, call it , has two objects and no non-identity morphisms. A diagram of this shape in picks out two objects of and says nothing about how they relate. Colimits over diagrams of this shape are coproducts, which we have met already; -object discrete shapes give -fold coproducts; and the empty shape gives an initial object.

The span shape, call it , has three objects and two non-identity morphisms, one from to and one from to , with no composites beyond identities. A diagram of this shape in picks out three objects and two morphisms and . Colimits over diagrams of this shape are pushouts, and they are the construction that does most of the book’s work.

Other shapes give other well-known colimits — a parallel-pair shape gives a coequaliser, an -chain shape gives a sequential colimit — but we will not need those here. The span is the one to keep in hand.

Cocones and colimits

The universal property of the coproduct, read carefully, generalises to any diagram. A coproduct of and was an object equipped with two morphisms out of and , universal among such equipments. In the general case the equipment is a family of morphisms indexed by the objects of the diagram’s shape.

A cocone under a diagram with apex is a natural transformation from to the constant functor that sends every object of to and every morphism to . Less economically: a cocone is a family of morphisms , one for each object of , subject to the requirement that for every morphism in we have

The are the cocone’s leg morphisms, and the equation says the legs are compatible with the shape’s arrows: if prescribes an arrow from to , then the leg at must factor through the leg at via the image of that arrow.

A span diagram in has three legs, , , , and two compatibility conditions, and . The two equations force to be determined by the other two legs, so a span cocone is effectively a pair satisfying . Most practical specifications of a pushout skip straight to this pair-with-equation formulation and omit the explicit third leg.

A colimit of a diagram is a universal cocone: an apex with leg morphisms such that every other cocone factors through by a unique morphism with for every . The uniqueness-up-to-isomorphism template of the previous chapter applies verbatim: two cocones both satisfying the universal property are canonically identified, and we speak of the colimit.

Coproducts drop out of this as the case when the shape is discrete: the universal factorisation from the previous chapter is exactly the unique morphism the general definition produces. Initial objects drop out as the case when the shape is empty. The general pattern includes them as the smallest cases, and the next section specialises to the three-object case that will get most of our attention.

Pushouts

A pushout is the colimit of a span.

Given a span , a pushout is an object together with morphisms and satisfying

and universal among such: every other pair into some object that satisfies the analogous equation factors through by a unique morphism . The picture is

Figure 4.1: the pushout square. The equation says the square commutes; universality says every other commuting square factors through this one by a unique .

One sentence of intuition. The pushout glues and together along their shared image of . If and are absent — if the span is really a discrete pair — the pushout collapses to the coproduct , which does no gluing. What and add is the requirement that the two images of , one in and one in , be identified in the pushout. is the smallest object in which they are, which is what one usually means by “gluing two things together along a shared part”.

Pushout in

In the category of sets, the pushout has an explicit construction. Take the disjoint union . For each , identify the element with the element . The quotient of the disjoint union by the equivalence relation generated by these identifications is the pushout, and the two injections and send an element to its equivalence class.

A minimal example: , , , , . The disjoint union has two elements, and ; the identification equates them; the pushout is a one-element set. Both injections land at the single element, and the original elements of and have been glued together along the common image from .

A slightly larger example makes the pattern visible. Let , , , with and . The disjoint union has six elements. The identifications equate with and with , but the starred elements of and are unaffected, since nothing in maps to them. The pushout has four elements: , , , .

This is what “gluing along” means concretely in : two copies of something, sharing a subset, welded at the subset. The same picture will return in Merge as pushout, where the “subset” is a common-ancestor schema and the “two copies” are the two branches’ schemas. There is no new construction there; there is only a different choice of category in which to take the same pushout.

When pushouts do not exist

Not every category has every pushout. does, and so do the categories of groups, topological spaces, vector spaces, and panproto schemas. does not in general, because the sum type that a pushout would demand might carry an equational constraint Haskell’s type system does not allow the programmer to express. A category has all pushouts if every span in it admits a pushout; the word for a category with all (small) colimits is cocomplete. Most of the categories we work with are cocomplete for the same reason is: their definitions allow the explicit constructions the abstract machinery asks for.

Pushout of theories

In the category of generalised algebraic theories — the subject of the next chapter — the pushout glues two theories along a common sub-theory.

Given a span , the pushout is a theory whose sorts and operations are the disjoint union of those in and , with sorts and operations in the image of identified along and . The construction parallels the set-theoretic one; the extra work is that the theories’ equations must be inherited, which functoriality of the translation takes care of.

This is the setting worked out at length in the institutions framework of Goguen & Burstall (1992), developed precisely to handle parametric combinations of logical and algebraic theories. An institution is, roughly, a category of theories together with a functor to its category of models, set up so that colimits of theories lift to operations on models in a controlled way. Panproto’s treatment of protocol composition is institutional in this sense, though we will not develop the full institutional machinery here; a reader who wants it can find it in Goguen and Burstall.

Pushouts in panproto

The two places where pushouts dominate panproto’s engineering are protocol composition and three-way merge.

Protocol composition

A protocol is a generalised algebraic theory together with a parser, an emitter, and a registration, as Protocols as theories, schemas as instances develops in detail. Two protocols that share a common sub-protocol combine by taking the pushout of the theories along the shared sub-theory.

The concrete case that makes this useful is when two protocols need to agree on some shared vocabulary. Panproto ships a protocol for ATProto lexicons and a separate one for Apache Avro schemas. Both represent records with named fields of declared types; both share a common sub-vocabulary of primitive types (strings, integers, booleans). A combined protocol accepting both formats, and translating between them at the boundaries, is the pushout of the two along the shared primitives sub-theory. The pushout is the place where the two vocabularies agree; the rest of each protocol sits above that shared substrate.

Panproto’s implementation lives in panproto_gat::colimit and panproto_schema::colimit. The pushout is verified by the usual property-based test: construct a cocone out of two arbitrary morphisms agreeing on the shared subprotocol, check that it factors through the computed pushout by a unique morphism, repeat across a sampled state space. Protocol colimits develops the machinery in full.

Three-way merge

The second and more visible application is the three-way merge operation at the heart of schematic version control.

Here the running example from Part I reappears. Two developers branch from a common-ancestor schema — our name-and-email address-record schema — and each evolves it independently. Developer A adds a phone field, producing . Developer B, working in parallel, renames email to contact_email, producing . The two branches need to merge.

Spelling out the span: , with the add-phone migration and the rename-email migration. The pushout is a schema containing every field of and every field of , with the fields inherited from (the original name, and email under its new name) appearing once. The injections and are the migrations from each branch into the merged schema; applied to each branch’s data, they yield the same records in the merged form.

Figure 4.2: three-way merge as a pushout. The two branch migrations and fan out from the common ancestor; the pushout glues them together along ’s shared content, and a record on either branch is carried into the merged schema by the appropriate injection.

Not every merge has a pushout. If A renames email to contact_email while B renames the same field to email_addr, the pushout would have to agree with both renamings on ’s email, which it cannot. Panproto’s merge algorithm detects this at the level of the universal property: the two branches present a cocone that does not factor uniquely through any candidate pushout, which means the pushout does not exist as a schema under the protocol. The engineering reading is that the merge has a genuine conflict, and the algorithm reports the exact disagreement — the two migrations cannot both commute with a third — rather than producing conflict markers in a text file.

The same pushout construction appears in the patch-theoretic treatment of textual merges due to Mimram & Giusto (2013), which informs the Darcs and Pijul systems (Roundy 2005). Panproto’s contribution is to apply the construction at the schema level rather than the byte level, which produces merge results that survive changes to the textual representation that have nothing to do with schema content. Merge as pushout specialises the present chapter’s construction to the implementation, including the handling of data stored under each branch.

Further reading

Mac Lane (1998), chapter III (“Universals and Limits”), is the canonical source for limits and colimits, with pushouts treated as a specific case. Chapter V (“Limits”) develops the general theory of diagrams. Awodey (2010), chapter 5 (“Limits and Colimits”), is the same material in a more undergraduate-facing register. Riehl (2016), chapter 3 (“Limits and Colimits”), is the modern reference. Leinster (2014) develops colimits in chapter 5 (“Limits”).

For the institutional perspective on pushouts of theories, Goguen & Burstall (1992) is foundational and Goguen (1991) its accompanying exposition. For the patch-theoretic view of merge, Mimram & Giusto (2013) is the nearest neighbour. The textbook-length account of algebraic specifications and theories combined by colimit is Sannella & Tarlecki (2012).

Closing

The next chapter introduces algebraic and generalised algebraic theories, the mathematical language in which panproto writes down what a protocol is.