Categorical algebra

Sets and Relations

The following APIs implement FinSet, the category of Finite Sets (actually the skeleton of FinSet). The objects of this category are natural numbers where n represents a set with n elements. The morphisms are functions between such sets. We use the skeleton of FinSet in order to ensure that all sets are finite and morphisms can be stored using lists of integers. Finite relations are built out of FinSet and can be used to do some relational algebra.


Category of (possibly infinite) sets and functions.

This module defines generic types for the category of sets (SetOb, SetFunction), as well as a few basic concrete types, such as a wrapper type to view Julia types as sets (TypeSet). Extensive support for finite sets is provided by another module, FinSets.


Abstract type for morphism in the category Set.

Every instance of SetFunction{<:SetOb{T},<:SetOb{T′}} is callable with elements of type T, returning an element of type T′.

Note: This type would be better called simply Function but that name is already taken by the base Julia type.


Abstract type for object in the category Set.

The type parameter T is the element type of the set.

Note: This type is more abstract than the built-in Julia types AbstractSet and Set, which are intended for data structures for finite sets. Those are encompassed by the subtype FinSet.


Forgetful functor Ob: Cat → Set.

Sends a category to its set of objects and a functor to its object map.


Function between finite sets.

The function can be defined implicitly by an arbitrary Julia function, in which case it is evaluated lazily, or explictly by a vector of integers. In the vector representation, the function (1↦1, 2↦3, 3↦2, 4↦3), for example, is represented by the vector [1,3,2,3].

This type is mildly generalized by FinDomFunction.


Finite set.

A finite set has abstract type FinSet{S,T}. The second type parameter T is the element type of the set and the first parameter S is the collection type, which can be a subtype of AbstractSet or another Julia collection type. In addition, the skeleton of the category FinSet is the important special case S = Int. The set ${1,…,n}$ is represented by the object FinSet(n) of type FinSet{Int,Int}.


Limit of finite sets viewed as a table.

Any limit of finite sets can be canonically viewed as a table (TabularSet) whose columns are the legs of the limit cone and whose rows correspond to elements of the limit object. To construct this table from an already computed limit, call TabularLimit(::AbstractLimit; ...). The column names of the table are given by the optional argument names.

In this tabular form, applying the universal property of the limit is trivial since it is just tupling. Thus, this representation can be useful when the original limit algorithm does not support efficient application of the universal property. On the other hand, this representation has the disadvantage of generally making the element type of the limit set more complicated.


Finite set whose elements are rows of a table.

The underlying table should be compliant with Tables.jl. For the sake of uniformity, the rows are provided as named tuples, which assumes that the table is not "extremely wide". This should not be a major limitation in practice but see the Tables.jl documentation for further discussion.


The rig of booleans.

This struct is needed because in base Julia, the product of booleans is another boolean, but the sum of booleans is coerced to an integer: true + true == 2.


Binary relation between finite sets.

A morphism in the category of finite sets and relations. The relation can be represented implicitly by an arbitrary Julia function mapping pairs of elements to booleans or explicitly by a matrix (dense or sparse) taking values in the rig of booleans (BoolRig).

Free Diagrams, Limits, and Colimts

The following modules define free diagrams in an arbitrary category and specify limit and colimt cones over said diagrams. Thes constructions enjoy the fullest support for FinSet and are used below to define presheaf categories as C-Sets. The general idea of these functions is that you set up a limit computation by specifying a diagram and asking for a limit or colimit cone, which is returned as a struct containing the apex object and the leg morphisms. This cone structure can be queried using the functions apex and legs. Julia's multiple dispatch feature is heavily used to specialize limit and colimit computations for various diagram shapes like product/coproduct and equalizer/coequalizer. As a consumer of this API, it is highly recommended that you use multiple dispatch to specialize your code on the diagram shape whenever possible.


Free diagrams in a category.

A free diagram in a category is a diagram whose shape is a free category. Examples include the empty diagram, pairs of objects, discrete diagrams, parallel pairs, composable pairs, and spans and cospans. Limits and colimits are most commonly taken over free diagrams.


A free diagram with a bipartite structure.

Such diagrams include most of the fixed shapes, such as spans, cospans, and parallel morphisms. They are also the generic shape of diagrams for limits and colimits arising from undirected wiring diagrams. For limits, the boxes correspond to vertices in $V₁$ and the junctions to vertices in $V₂$. Colimits are dual.


Convert a free diagram to a bipartite free diagram.

Reduce a free diagram to a free bipartite diagram with the same limit (the default, colimit=false) or the same colimit (colimit=true). The reduction is essentially the same in both cases, except for the choice of where to put isolated vertices, where we follow the conventions described at cone_objects and cocone_objects. The resulting object is a bipartite free diagram equipped with maps from the vertices of the bipartite diagram to the vertices of the original diagram.


Bundle together legs of a multi(co)span.

For example, calling bundle_legs(span, SVector((1,2),(3,4))) on a multispan with four legs gives a span whose left leg bundles legs 1 and 2 and whose right leg bundles legs 3 and 4. Note that in addition to bundling, this function can also permute legs and discard them.

The bundling is performed using the universal property of (co)products, which assumes that these (co)limits exist.


Objects in diagram that will have explicit legs in limit cone.

In category theory, it is common practice to elide legs of limit cones that can be computed from other legs, especially for diagrams of certain fixed shapes. For example, when it taking a pullback (the limit of a cospan), the limit object is often treated as having two projections, rather than three. This function encodes such conventions by listing the objects in the diagram that will have corresponding legs in the limit object created by Catlab.

See also: cocone_objects.


Meta-algorithm that reduces general limits to common special cases.

Reduces limits of free diagrams that happen to be discrete to products. If this fails, fall back to the given algorithm (if any).

TODO: Reduce free diagrams that are (multi)cospans to (wide) pullbacks.


Colimit of a diagram.

To define colimits in a category with objects Ob, override the method colimit(::FreeDiagram{Ob}) for general colimits or colimit(::D) with suitable type D <: FixedShapeFreeDiagram{Ob} for colimits of specific shape, such as coproducts or coequalizers.

See also: limit


The image and coimage are isomorphic. We get this isomorphism using univeral properties.

  CoIm′ ╌╌> I ↠ CoIm
    ┆ ⌟     |
    v       v
    I   ⟶  R ↩ Im
    |       ┆
    v    ⌜  v
    R ╌╌> Im′

Limit of a diagram.

To define limits in a category with objects Ob, override the method limit(::FreeDiagram{Ob}) for general limits or limit(::D) with suitable type D <: FixedShapeFreeDiagram{Ob} for limits of specific shape, such as products or equalizers.

See also: colimit


Pullback of a pair of morphisms with common codomain.

To implement for a type T, define the method limit(::Cospan{T}) and/or limit(::Multicospan{T}) or, if you have already implemented products and equalizers, rely on the default implementation.


Pushout of a pair of morphisms with common domain.

To implement for a type T, define the method colimit(::Span{T}) and/or colimit(::Multispan{T}) or, if you have already implemented coproducts and coequalizers, rely on the default implementation.


Coequalizer of morphisms with common domain and codomain.

To implement for a type T, define the method colimit(::ParallelPair{T}) or colimit(::ParallelMorphisms{T}).


Copairing of morphisms: universal property of coproducts/pushouts.

To implement for coproducts of type T, define the method universal(::BinaryCoproduct{T}, ::Cospan{T}) and/or universal(::Coproduct{T}, ::Multicospan{T}) and similarly for pushouts.


Coproduct of objects.

To implement for a type T, define the method colimit(::ObjectPair{T}) and/or colimit(::DiscreteDiagram{T}).


Unique morphism out of an initial object.

To implement for a type T, define the method universal(::Initial{T}, ::SMulticospan{0,T}).


Unique morphism into a terminal object.

To implement for a type T, define the method universal(::Terminal{T}, ::SMultispan{0,T}).


Equalizer of morphisms with common domain and codomain.

To implement for a type T, define the method limit(::ParallelPair{T}) and/or limit(::ParallelMorphisms{T}).


Factor morphism through (co)equalizer, via the universal property.

To implement for equalizers of type T, define the method universal(::Equalizer{T}, ::SMultispan{1,T}). For coequalizers of type T, define the method universal(::Coequalizer{T}, ::SMulticospan{1,T}).


Initial object.

To implement for a type T, define the method colimit(::EmptyDiagram{T}).


Pairing of morphisms: universal property of products/pullbacks.

To implement for products of type T, define the method universal(::BinaryProduct{T}, ::Span{T}) and/or universal(::Product{T}, ::Multispan{T}) and similarly for pullbacks.


Product of objects.

To implement for a type T, define the method limit(::ObjectPair{T}) and/or limit(::DiscreteDiagram{T}).


Terminal object.

To implement for a type T, define the method limit(::EmptyDiagram{T}).


Universal property of (co)limits.

Compute the morphism whose existence and uniqueness is guaranteed by the universal property of (co)limits.

See also: limit, colimit.



2-category of categories, functors, and natural transformations.

Categories in mathematics appear in the large, often as categories of sets with extra structure, and in the small, as algebraic structures that generalize groups, monoids, preorders, and graphs. This division manifests in Catlab as well. Large categories (in spirit, if not in the technical sense) occur throughout Catlab as @instances of the theory of categories. For computational reasons, small categories are usually presented by generators and relations.

This module provides a minimal interface to accomodate both situations. Category instances are supported through the wrapper type TypeCat. Finitely presented categories are provided by another module, FinCats.


Abstract base type for a category.

The objects and morphisms in the category have Julia types Ob and Hom, respectively. Note that these types do not necessarily form an @instance of the theory of categories, as they may not meaningfully form a category outside the context of this object. For example, a finite category regarded as a reflexive graph with a composition operation might have type Cat{Int,Int}, where the objects and morphisms are numerical identifiers for vertices and edges in the graph.

The basic operations available in any category are: dom, codom, id, compose.


Abstract base type for a functor between categories.

A functor has a domain and a codomain (dom and codom), which are categories, and object and morphism maps, which can be evaluated using ob_map and hom_map. The functor object can also be called directly when the objects and morphisms have distinct Julia types. This is sometimes but not always the case (see Category), so when writing generic code one should prefer the ob_map and hom_map functions.


Abstract base type for a natural transformation between functors.

A natural transformation $α: F ⇒ G$ has a domain $F$ and codomain $G$ (dom and codom), which are functors $F,G: C → D$ having the same domain $C$ and codomain $D$. The transformation consists of a component $αₓ: Fx → Gx$ in $D$ for each object $x ∈ C$, accessible using component or indexing notation (Base.getindex).


Are two morphisms in a category equal?

By default, just checks for equality of Julia objects using $==$. In some categories, checking equality of morphisms may involve nontrivial reasoning.


Oppositization 2-functor.

The oppositization endo-2-functor on Cat, sending a category to its opposite, is covariant on objects and morphisms and contravariant on 2-morphisms, i.e., is a 2-functor $op: Catᶜᵒ → Cat$. For more explanation, see the nLab.


Coerce or look up object in category.

Converts the input to an object in the category, which should be of type Ob in a category of type Cat{Ob,Hom}. How this works depends on the category, but a common case is to look up objects, which might be integers or GAT expressions, by their human-readable name, usually a symbol.

See also: hom.


2-category of finitely presented categories.

This module is for the 2-category Cat what the module FinSets is for the category Set: a finitary, combinatorial setting where explicit calculations can be carried out. We emphasize that the prefix Fin means "finitely presented," not "finite," as finite categories are too restrictive a notion for many purposes. For example, the free category on a graph is finite if and only if the graph is DAG, which is a fairly special condition. This usage of Fin is also consistent with FinSet because for sets, being finite and being finitely presented are equivalent.


Category presented by a finite graph together with path equations.

The objects of the category are vertices in the graph and the morphisms are paths, quotiented by the congruence relation generated by the path equations. See (Spivak, 2014, Category theory for the sciences, §4.5).


Category defined by a Presentation object.

The presentation type can, of course, be a category (Theories.Category). It can also be a schema (Theories.Schema). In this case, the schema's objects and attribute types are regarded as the category's objects and the schema's morphisms, attributes, and attribute types as the category's morphisms (where the attribute types are identity morphisms). When the schema is formalized as a profunctor whose codomain category is discrete, this amounts to taking the collage of the profunctor.


A natural transformation whose domain category is finitely generated.

This type is for natural transformations $α: F ⇒ G: C → D$ such that the domain category $C$ is finitely generated. Such a natural transformation is given by a finite amount of data (one morphism in $D$ for each generating object of $C$) and its naturality is verified by finitely many equations (one equation for each generating morphism of $C$).


Coerce or look up morphism generator in a finitely presented category.

Since morphism generators often have a different data type than morphisms (e.g., in a free category on a graph, the morphism generators are edges and the morphisms are paths), the return type of this function is generally different than that of hom.


Is the purported functor on a presented category functorial?

This function checks that functor preserves domains and codomains. When check_equations is true (the default), it also checks that the functor preserves all equations in the domain category. Note that in some cases this may not be possible.

See also: is_natural.


Dual to a final functor, an initial functor is one for which pulling back diagrams along it does not change the limits of these diagrams.

This amounts to checking, for a functor C->D, that, for every object d in Ob(D), the comma category (F/d) is connected.


Is the transformation between FinDomFunctors a natural transformation?

This function uses the fact that to check whether a transformation is natural, it suffices to check the naturality equations on a generating set of morphisms of the domain category. In some cases, checking the equations may be expensive or impossible. When the keyword argument check_equations is false, only the domains and codomains of the components are checked.

See also: is_functorial.


Coerce or look up object generator in a finitely presented category.

Because object generators usually coincide with objects, the default method for ob in finitely presented categories simply calls this function.


Object generators of finitely presented category.

The object generators of finite presented category are almost always the same as the objects. In principle, however, it is possible to have equations between objects, so that there are fewer objects than object generators.


Overview and Theory

For an in depth look into the theory behind acsets, we refer the reader to our paper on the subject, which also gives some sense as to how the implementation works. Here, we give a brief tutorial before the the API documentation.

The most essential part of the acset machinery is the schema. The schema parameterizes the acset: each schema corresponds to a different type of acset. Schemas consist of four parts.

  • Objects $X,Y$ ((X,Y,Z)::Ob)
  • Homomorphisms $f \colon X \to Y$ (f :: Hom(X,Y)), which go from objects to objects
  • Attribute types $\mathtt{T}$ (T :: AttrType)
  • Attributes $a \colon X \to \mathtt{T}$ (a :: Attr(X,T)), which go from objects to data types

For those with a categorical background, a schema is a presentation of a category $|S|$ along with a functor $S$ from $|S|$ to the arrow category $0 \to 1$, such that $S^{-1}(1)$ is discrete.

An acset $F$ on a schema consists of...

  • a set $F(X)$ of "primary keys" for each object
  • a function $F(f) \colon F(X) \to F(Y)$ for each morphism
  • a Julia data type $F(\mathtt{T})$ for each data type $\mathtt{T}$
  • a function $F(a) \colon F(X) \to F(\mathtt{T})$ for each attribute $a$.

For those with a categorical background, an acset on a schema $S$ consists of a functor from $S$ to $\mathsf{Set}$, such that objects in $S^{-1}(0)$ map to finite sets, and objects in $S^{-1}(1)$ map to sets that represent types. For any particular functor $K \colon S^{-1}(1) \to \mathsf{Set}$, we can also take the category of acsets that restrict to this map on $S^{-1}$.

We can also add equations to this presentation, but we currently do nothing with those equations in the implementation; they mostly serve as documentation.

We will now give an example of how this all works in practice.

using Catlab, Catlab.CategoricalAlgebra

# Write down the schema for a weighted graph
@present SchWeightedGraph(FreeSchema) begin

# Construct the type used to store acsets on the previous schema
# We *index* src and tgt, which means that we store not only
# the forwards map, but also the backwards map.
@acset_type WeightedGraph(SchWeightedGraph, index=[:src,:tgt])

# Construct a weighted graph, with floats as edge weights
g = @acset WeightedGraph{Float64} begin
  V = 4
  E = 5
  src = [1,1,1,2,3]
  tgt = [2,3,4,4,4]
  weight = [7.2, 9.3, 9.4, 0.1, 42.0]
Main.__atexample__356.WeightedGraph{Float64} with elements V = 1:4, E = 1:5
E src tgt weight
1 1 2 7.2
2 1 3 9.3
3 1 4 9.4
4 2 4 0.1
5 3 4 42.0


The mathematical abstraction of an acset can of course be implemented in many different ways. Currently, there are three implementations of acsets in Catlab, which share a great deal of code.

These implementations can be split into two categories.

The first category is static acset types. In this implementation, different schemas correspond to different Julia types. Methods on these Julia types are then custom-generated for the schema, using CompTime.jl.

Under this category, there are two classes of static acset types. The first class is acset types that are generated using the @acset_type macro. These acset types are custom-derived structs. The advantage of this is that the structs have names like Graph or WiringDiagram that are printed out in error messages. The disadvantage is that if you are taking in schemas at runtime, you have to eval code in order to use them.

Here is an example of using @acset_type

@acset_type WeightedGraph(SchWeightedGraph, index=[:src,:tgt])
g = WeightedGraph()

The second class is AnonACSets. Like acset types derived from @acset_type, these contain the schema in their type. However, they also contain the type of their fields in their types, so the types printed out in error messages are long and ugly. The advantage of these is that they can be used in situations where the schema is passed in at runtime, and they don't require using eval to create a new acset type.

Here is an example of using AnonACSet

const WeightedGraph = AnonACSetType(SchWeightedGraph, index=[:src,:tgt])
g = WeightedGraph()

The second category is dynamic acset types. Currently, there is just one type that falls under this category: DynamicACSet. This type has a field for the schema, and no code-generation is done for operations on acsets of this type. This means that if the schema is large compared to the data, this type will often be faster than the static acsets.

However, dynamics acsets are a new addition to Catlab, and much of the machinery of limits, colimits, and other high-level acset constructions assumes that the schema of an acset can be derived from the type. Thus, more work will have to be done before dynamic acsets become a drop-in replacement for static acsets.

Here is an example of using a dynamic acset

g = DynamicACSet("WeightedGraph", SchWeightedGraph; index=[:src,:tgt])
Modules = [
Private = false