Thanks to languages like Haskell, type classes have become a popular form of code modularlization. If one were to due a quick google search, countless articles and blog posts shine light onto clever ways type classes have been used in applied areas. However, there seems to be a lack of interest in the foundational aspect of type classes themselves. Many people do not seem to know the mathamatical meaning behind them and often see them as interchangeable with “Functor”-Module systems (which is something I am not confident in agreeing with). In addition, usual type classes, as those used in Haskell, fall a bit short in describing what mathamatical classes can do themselves. To understand type classes better, we will both look at mathamatical classes and type-classes in the language Coq.

Mathamatical classes

Initially, in early set theory, the word ‘class’ was a synonym for ’set’. The word ‘class’ was often used to describe ’sets’ that were defined by quantification over sets, i.e all sets where some preposition is true. Unfortunately, this form of quantification led to paradoxes, thus breaking the whole system (see Russell’s paradox). This led to two stricter and importantly different definitions of both sets and classes. It was decided that sets could no longer be defined using quantification over other sets and only classes could be defined in such a way. It was also stated that classes do not have sub-classes and cannot quatify over other classes. Classes are thus more general than sets and sets had become more finite. This is shown by the fact that some classes are sets but not all classes are sets. In fact, classes not only quantify over sets but they can quantify over any mathamatical object (that are not classes). For our use cases, we will be using types as our mathamatical objects.

In all fairness, I am not a mathamatician and the previous explanaition is very much simplified; there are further nuances about classes that I for sure have not touched upon.

Describing structures via their properties

Here is a screenshot of a graph from the wikipedia page on Semigroups. The y-axis marks algebraic structures and the x-axis marks the poperties that define each structure.

Each structure on the left can be seen as quantifications over all types that satisfy their marked properties. Let’s code some of them up using classes.

Type-classes in Coq

One of the most important aspects of Coq is that every function terminates (i.e is total). Thus the property of totality is automatically satisfied without proof.

Now let’s start with SemiGroups. The wonderful thing that Coq can do and that Haskell cannot, is that Coq can preserve properties consistently and correctly within the type system. Here is the type class for SemiGroup in Coq.

Class SemiGroup A : Type := {

f : A -> A -> A;

assoc_f : forall a b c : A, f a (f b c) = f (f a b) c

}.

SemiGroups are both total (check) and associative. Look at how the class definition requires both the function and the property that it holds. This, in fact, is just a regular mathamatical class definition understood by a compiler.

Now let’s look at Monoids. Monoids are SemiGroups with an identity element.

Class Monoid A `{SemiGroup A} := {

id : A ;

proof_id : forall a : A, f a id = a

}.

See how the function ‘f’ is used in this definition? It is referenced from the implicit assumption (that we gave) that A also forms a SemiGroup.

In Haskell, we would be done defining classes since there is no way to represent a Group with Haskell’s type class system. But hey, this is Coq, so we can!

Class Group A `{Monoid A} := {

group_proof : forall a : A, exists b : A, f a b = id

}.

Here you can see, with accordance to that chart, that every group is a monoid with inverse operation/elements. Just like with ‘f’, ‘id’ is infered from the assumed Monoid instance.

Now let’s go even further and provide a AbelianGroup Class.

Class AbelianGroup A `{Group A} := {

commute_proof : forall a b : A, f a b = f b a

}.

It is also shown here that an abelian group is just a group that also commutes.

Now here is how Z.t (a Coq dataType representation for Ints) is an instance of all of these classes over the function Z.add.

Instance SemiInt : SemiGroup Z.t := {

f := Z.add;

assoc_f := Z.add_assoc

}. Instance MonoidInt : Monoid Z.t := {

id := 0;

proof_id := Z.add_0_r

}. Instance GroupInt : Group Z.t := {}.

Proof.

intros.

pose (witness := (Z.opp a)).

refine (ex_intro _ witness _).

simpl.

unfold witness.

case a.

simpl.

unfold id ; auto.

exact Z.pos_sub_diag.

simpl.

exact Z.pos_sub_diag.

Defined. Instance AbelianInt : AbelianGroup Z.t := {

commute_proof := Z.add_comm

}.

Conclusion

Type classes can do a whole lot for us as programmers. But we most know the exact reason why we use them in the first place.

The two reasons for using type classes are mathematical clarity and productive code reuse. Type classes are mathematically clear because they are exactly just mathematical classes. They also provide code reuse by placing useful proofs and code into a more confined and designated area and also by overwriting functions with multiple definitions.

The use them to their fullest extend one would need dependent types in order to provide a more mathematical and guided approach to programming. Coq allows us to program safely in higher-orders that we could not in most other languages.

Hopefully in the future we can have a unified collection of dependently-typed/integrated typeclasses that would connect everything from Monads to Rings in a logical and modular manner. However, this task may be harder than it seems.