I always find it interesting to track which parts of a definition (mathematical, etc.) are uniquely determined by the other parts. This is a quasi-combinatoric view that data is determined by freedom to make choices, and everything else, while informative in one sense of providing properties/boundaries/restrictions, does not actually provide the information that characterizes an object. Itʼs applicable in many contexts, and Iʼll just go through some hand-wavey examples to show you how I think about it.
Note that I will be assuming univalence in this post, which means isomorphic types are equal. This greatly simplifies how types are treated, as you will see.
Letʼs take the algebraic hierarchy of magmas to groups as our first set of examples. At each step we will discuss what each piece of data brings to the picture.
Magmas have a carrier type T : Set
(an h-set) and a binary
operation op : T → T → T
. In code it would be
magma := Σ(T : Set), T → T → T
, or as a structure in
Lean-prover syntax:
structure Magma :=
(T : Set)
(op : T → T → T)
It turns out that the type doesnʼt matter as much as the operation
does, especially once it is wrapped in a magma that hides the type (I
will try to justify this later in the section on categories). For any
equivalent types T1
and T2
, you can take an
operation op1 : T1 → T1 → T1
and define an equivalent
operation op2 : T2 → T2 → T2
by transporting along the
equivalence as needed. This is esssentially the definition of what it
means for two operations on possibly different types to be equivalent –
that is, for two magmas to be equal:
Π(T1 T2 : Set), Π(op1 : T1 → T1 → T1), Π(op2 : T2 → T2 → T2),
(Magma.mk T1 op1 = Magma.mk T2 op2) = Σ(t : T1 = T2), Π(a b : T1), apply t (op1 a b) = op2 (apply t a) (apply t b)
In words this means for all T1
, T2
,
op1
, and op2
as above, the corresponding
magmas are equal precisely when the types are equal and the operations
are also equal when transported along that equality. (Note that due to
proof-relevance in HoTT, there may be many ways of establishing an
equality between types, so magmas on the same type may be equal in
interesting ways.)
Of course, since we are working in type theory, the type matters, but really it is determined by the function (two functions could not be considered equivalent in any reasonable sense if their types were not equivalent), so for the purposes of our argument, we will treat is as uniquely determined by the operation.
Thus a magma is essentially determined by its operation.
Semigroups are magmas whose operation is associative:
structure Semigroup :=
(T : Set)
(op : T → T → T)
(notation a `•`:65 b:65 := op a b)
(assoc : Π(a b c : T), a • (b • c) = (a • b) • c)
Note that since T
is an h-set
,
assoc
will be an h-prop
, which means it
contributes no data, only a unique proof. So if you accept that magmas
are uniquely determined by their operation, semigroups are still
uniquely determined by that.
Monoids are semigroups with an identity element i
:
structure Monoid :=
(T : Set)
(op : T → T → T)
(notation a `•`:65 b:65 := op a b)
(assoc : Π(a b c : T), a • (b • c) = (a • b) • c)
(i : T)
(identity : Π(a : T), a • i = a ∧ i • a = a)
Now we are adding data, i : T
, which looks like it could
be any element of T
, an arbitrarily large type! But it
turns out that it is still uniquely determined by the operation: for
identities i1 i2 : T
, we have a proof that
i1 • i2 = i1
(right identity of i2
) and
another that i1 • i2 = i2
(left identity of
i1
), so it must be that i1 = i2
.
Another way to formulate it would be with the following sigma type
replacing the separate fields i
and
identity
:
def IdentityOf {T : Type} (op : T → T → T) :=
Σ(i : T), Π(a : T), op a i = a ∧ op i a = a
And then one could prove that IdentityOf op
is a
subsingleton, i.e. h-prop.
So we see that each operation has at most one identity. So a monoid describes a magma with some proofs of nice properties, but no additional choices were made after deciding what operation to use (and on what type).
How will it look for groups?
Groups are monoids with inverses:
structure Groups :=
(T : Set)
(op : T → T → T)
(notation a `•`:65 b:65 := op a b)
(assoc : Π(a b c : T), a • (b • c) = (a • b) • c)
(i : T)
(identity : Π(a : T), a • i = a ∧ i • a = a)
(inv : T → T)
(inverse : Π(a : T), a • inv a = i ∧ inv a • a = i)
Again, thereʼs a standard abstract algebra proof that inverses are
unique, which means the function inv
witnessing the
totality of the inverse relation is also unique, so again, being a group
is a property of the magma operation, there are no additional choices
made in the definition of a group.
Like before, inv
and inverse
could be
bundled into a subsingleton sigma type like so:
def InverseOf {T : Type} (op : T → T → T) (i : T) :=
Σ(inv : T → T), Π(a : T), op a (inv a) = i ∧ op (inv a) a = i
Here weʼve been studying magmas &c. as mathematical objects, and we saw that they were HoTT-equal when their operations were equivalent under the change of type, and, as a related fact, that the extra fields of monoids/groups added information about properties of the operation but no additional data (i.e. choices).
However, these algebraic structures are most often used via
typeclasses, in programming languages like Haskell and theorem provers
like Lean, where they are parameterized over their carrier type:
class magma (T : Type) := (op : T → T → T)
, etc. In this
way, an instance of the class provides a canonical choice of operation
for a particular (nominal) type, and easy/implicit access to the
properties like associativity. This can be great for brevity, but can be
a challenge where there are multiple natural choices for an operation on
the same type, such as addition/multiplication, or when you run into
other dark corners of typeclass issues (like diamonds, where the same
type can have two different instances from generic derivations).
But as it turns out, most of these issues go away when you
parameterize the classes by the operation (and leave the type implicit),
like
class semigroup {T : Type} (op : T → T → T) := (assoc : _)
.
All the data thatʼs hidden away through the implicit resolution of the
typeclass system is no surprise, as it can only follow from the
operator, so there wonʼt be any chance of two incompatible instances
being inferred (although one could still run into issues with a lack of
definition equality in identities and inverses).
Furthermore, I would also argue that this kind of structure
philosophically fits better in a Univalent universe, because it acts
more like a well-defined-but-partial function from
Magma -> {Group,Monoid,Semigroup,...}
, one that respects
type equalities instead of making decisions based on type names.
While the operators would have to be explicit with this setup, it is
compatible with the existence of canonical choices (e.g. a
+
operator could still be disambiguated by type), and it
still retains the ability for identities and inverses to be implicit in
some sense (since they are not parameters to the structure), but
syntactically disambiguating the identites would get harder in
general.
On a final note, of course these results generalize to other algebraic structures, such as the hierarchy from rigs/semirings and rings to fields, which would need to be parameterized over the additive and multiplicative operators together.
Letʼs apply what weʼve learned to (small) categories, which are much richer objects than magmas. Start with the laws:
structure CategoryLaws (obj : Set) (hom : obj → obj → Set)
(comp : Π{a b c : obj}, hom a b → hom b c → hom c d)
(id : Π(o : obj), hom o o) :=
(left_id := Π{a b : obj} (f : hom a b), comp id f = f)
(right_id := Π{a b : obj} (f : hom a b), comp f id = f)
(comp_assoc := Π{a b c d : obj} (f : hom a b) (g : hom b c) (h : hom c d), comp f (comp g h) = comp (comp f g) h)
Now a category, as a mathematical object, looks like
structure Category :=
(obj : Set)
(hom : obj → obj → Set)
(comp : Π{a b c : obj}, hom a b → hom b c → hom c d)
(id : Π(o : obj), hom o o)
(lawful : CategoryLaws obj hom comp id)
What information characterizes this? Well, as we were saying earlier,
it isnʼt obj
, since it can be inferred from
hom
. Objects are essential for the form and structure of a
category, dictating which morphisms can be composed, but they are not
the important part. This is exactly what category theorists have been
saying for decades: itʼs the arrows that matter, not the objects!
But wait a second, is that really right? Actually, I would argue that
comp
is what determines the category – since
hom
can be inferred from that! This seems a little weird,
since we very rarely talk about what composition looks like in a
category. This is because it is usually determined by hom
and parametricity (e.g. for functions, there really is only one way to
parametrically compose them, the obvious combinator
forall a b c. (a -> b) -> (b -> c) -> (a -> c)
),
and once you add in the restrictions on associativity and identity,
there is usually a unique choice (I think relations fall into this
category) or an canonical “interesting” choice.
Of course, id
does not contribute information (it is
uniquely determined by comp
).
So it looks like hom
often determines what category we
are talking about, but it is really comp
. All the rest of
the fields serve to constrain comp
in some way, saying
which homs it needs to be able to relate, or what properties it should
have.
In fact, another way to look at categories would be to take the
comp
operation to be partial not in the sense of requiring
its arguments to have type indices that align
(forall a b c. hom a b -> hom b c -> hom a c
), but
being able to return nothing for certain combinations of arguments
(hom -> hom -> Maybe hom
). This looks more like a
magma operation, but the indices need to be recovered with
dom : hom -> obj
and cod : hom -> obj
which define where composition is allowed:
comp f g = Nothing
iff cod f = dom g
. It turns
out that this is less convenient to work with, but it is mathematically
equivalent with the right rules (e.g. id : obj -> hom
and comp f (id (cod f)) = Just f
). Hopefully this helps
clarify why comp
is the locus of information in a
category.
The heuristics used informally above gathered in one place: 1. Consider types to be non-informative when they can be inferred from later arguments (so pretty much most of the time). Behavior is determined by functions and terms, not by types! 2. Proofs (of h-props) are non-informative. Usually these are pretty obvious. 3. Sometimes terms, even functions, are non-informative when there are restrictions on them that make them unique (e.g. identities and inverses of operations). This will require more careful examination and arguments than the other cases but is definitely worth figuring out.