The search for the chimera of perfect code has motivated software engineers for years. While building complex software, we are inevitably confronted with code that is hard to understand, hard to maintain, and easy to break.

Many a professional have been confronted with complex systems where small changes apparently work, compile, pass all the tests, but then introduce some subtle data races which manifest themselves as bugs in an unpredictable fashion. For this reason, our primary goal will be the introduction of (type) safety in our programs: we want to make as much use as possible of the compiler in order to guarantee that changes to the code have no negative effect on our program. If a mistake is made, we want to know at compile-time in the form of a compiler error.

Most developers also share another common experience, which is enough to give even the most seasoned professional the shivers: huge, strangely designed object-oriented libraries with layers upon layers of design patterns. Adding a simple feature might thus require layers and layers of unfathomable conversions, leading us to infamous constructs such as FactorySingletonFactoryObserverAdapter which end up introducing abstractions much more complex than the feature they are used for. For this reason, our secondary goal will be the use of small abstractions. Defining an abstraction as simple would be too ambitious, since simplicity is entirely determined by the user’s experience and level of knowledge. Instead, we will go for a more attainable goal: building small abstractions which takes us from zero (builtin primitive) to a hundred (modeling concurrent processes) in very little code.

In the rest of the article, we will start by exploring the basics of a relatively recent discipline, Category theory. We will define some basic concepts such as what a category is, and show how categories can be easily interpreted as programming languages with types and functions. We will then introduce the notion of Bi-Cartesian Closed Categories, which introduce a minimal set of “generic interfaces” which can be used to implement virtually all of the interesting data types we might think of. The topics discussed are all implementable and can be used in practice. Moreover, they are actually already implemented as part of the ts-BCCC TypeScript library (repo and npm).

Basic Category theory

Category theory was born as a meta-field in mathematics, with the goal to introduce general concepts that would prove useful to practitioners within different sub-domains of mathematics. Many theories and theorems had been proven for different fields (geometry, logic, etc.), but many suspected such theories not only to be related, but to actually be the same. Category Theory was indeed successful in mathematics, showing that a “grand unification” of apparently unrelated concepts is possible. Unfortunately, this comes at a cost: all concepts and definitions of Category Theory may not introduce unnecessary concreteness and structure, in order to remain as broadly applicable as possible. Between a more concrete definition and a more abstract definition, the more abstract definition is always chosen. This leads many categorical concepts to feel a bit “disconnected” from practical application.

This is perhaps visible right away. A category is defined as a very simple structure, almost void of any unnecessary frills. Categories only contain objects, and arrows between them:

Objects: a, b, c, ...

Arrows: f, g, h, ...

Arrows connect objects. We say f:a->b (“ f goes from a to b ”) to denote that f connects objects a and b . Fortunately for us, this abstraction does not constitute such a problem for programmers, because categories resemble some very familiar concepts that software developers deal with on a daily basis: programming languages. It quickly becomes evident how arbitrary categories can easily be translated into programming languages with objects as types and arrows as functions. Therefore, f:a->b can also be seen as a function from type a to type b .

One of the most powerful aspects of categories is that they already have a built-in mechanism for composing different arrows into new arrows. Given two arrows with “compatible endpoints”, we can always build a new arrow which merges them:

f : a -> b

g : b -> c

f;g : a -> c

In the snippet above we used the ; operator to denote that first we would evaluate f , thereby producing a b which would then be fed right into g . The output of g is then returned as final output. f;g is read “ f , then g ”. Mathematicians prefer the use of the function composition symbol, which requires swapping the functions: g . f is an equivalent formulation of the composition of f and g , and it is read “ g after f ”. Since we are focusing on programming, we will prefer the ; operator since it shows explicitly that we are building pipelines of functions where the order of evaluation of the pipeline goes left-to-right.

Arrow (or function) composition is obviously not limited to an arbitrary depth. For example, we could compose more than two functions:

f : a -> b

g : b -> c

h : c -> d

f;g;h : a -> d

Notice that function composition is associative, thus we do not need brackets to specify the order of association.

In order to better understand this, let us provide an implementation in TypeScript. Implementing often offers us a new and more concrete perspective on all the moving parts, and how they are used.

Let us define a generic Fun<a,b> datatype which encapsulates a categorical function from an arbitrary type a into an arbitrary type b :

export interface Fun<a,b> {

f:(_:a) => b

after:<c>(f:Fun<c,a>) => Fun<c,b>

then:<c>(f:Fun<b,c>) => Fun<a,c>

}

A categorical function is a wrapper around an actual “regular” TypeScript function, f . Moreover, Fun also contains two methods, then and after , which both allow composing the current function with other functions. then post-composes, whereas after pre-composes.

In order to create a Fun , all we need is an actual function which we then augment with the required extra fields:

export let fun = <a,b>(f:(_:a) => b) : Fun<a,b> => ({

f:f,

after: function<c>(this:Fun<a,b>, g:Fun<c,a>) : Fun<c,b> {

return fun<c,b>((x) => this.f(g.f(x))) },

then: function<c>(this:Fun<a,b>, g:Fun<b,c>) : Fun<a,c> {

return fun<a,c>((x) => g.f(this.f(x))) }

})

We can thus use our Fun interface as follows:

let incr:Fun<number,number> = fun(x => x + 1)

let is_even:Fun<number,boolean> = fun(x => x % 2 == 0)

let not:Fun<boolean,boolean> = fun(x => !x)

Thanks to composition, we could even define new functions from existing functions quite easily. For example, a function which takes as input a number, increments it, then checks whether or not it is even, and finally swaps the result, would simply look like:

let f:Fun<number,boolean> = incr.then(is_even.then(not))

Notice that the way we would read the code out loud, and the way we interpret the code, overlap significantly thanks to the left-to-right reading order and the use of then as an operator.

Associativity also has a powerful implication: the function above could also be written with another order of composition, without any observable difference:

let f = incr.then(is_even).then(not)

Indeed, both definitions would first invoke incr , then is_even , and finally not .

Thanks to this basic formulation, it is possible to create very large pipelines of functions. This can come in handy in many concrete situations. For example, a React render function typically starts from both state and properties, which we could see as a single large datatype SP , and produce a renderable component. We could therefore see the render function of React as simply

render : Fun<SP, JSX.Element>

Suppose that we only wanted to show a div with only the Name of the Props inside SP . Then, with only composition, we could write:

render = get_props.then(get_name).then(render_div)

Whenever we have a pipeline with a clear entry-point and a clear result, using function composition will make our life easier and our code short and linear (and to some perhaps also quite elegant).

A powerful standard library: Bi-Cartesian Closed Categories

Categorical reasoning has so far produced a pretty, but limited result: a way to compose functions into arbitrarily long pipelines. This is not much, but fortunately there is much more that we can add to our framework. From more theoretical areas of programming (logic and the lambda calculus) we know that a programming language achieves maximum expressive power when it allows us to combine arbitrary types together into some specific new types which, together, allow us to design anything. Specifically, given two arbitrary types a and b , we can combine them as follows:

both a and b together in a tuple a*b (JavaScript objects, the fields of a Java class, the fields of an SQL record, are all essentially tuples);

and together in a tuple (JavaScript objects, the fields of a Java class, the fields of an SQL record, are all essentially tuples); either a or b in a discriminated union a+b (polymorphism in object-oriented languages, tagged unions in C, are all essentially instances of this pattern);

or in a discriminated union (polymorphism in object-oriented languages, tagged unions in C, are all essentially instances of this pattern); a function from a to b a => b .

The certainty that these types are all we need to build also very complex systems should be enough to keep us very interested. Ideally, after finding a small set of design patterns which are independent from each other (and thus represent separate concepts), can be composed with each other, and is enough to build anything, our goal immediately shifts towards building a standard library out of them.

Unfortunately, the definitions given above are a bit vague. From previous experience and intuition, we could imagine that we might extract the elements of a tuple, pattern match or visit a union, and invoke a function. Unfortunately, intuition is unclear and error prone, and does not help much when trying to provide a solid basis for implementation, especially of a standard library.

Category theory helps bringing clarity to the picture. The emphasis of categories lies not on the objects themselves, but rather on the arrows between objects. Category theory does not concern itself with the specific implementation of a construct such as the tuple number * boolean , but rather it dictates how its methods are supposed to work when composed with other methods.

Tuples (products)

Let us start with tuples, which dictate how we can join multiple values of different types into a single larger container. Saying that Person = { name:string, surname:string } is little more than a fancy way of saying that a Person is a tuple of two strings. Tuples are known as products in Category Theory.

We say that a category has (categorical) products if, for all arbitrary objects (types) a and b , there exists an object a*b . In addition to the object a*b , there exist a series of functions to construct, destroy, and transform instances of a*b .

Construction tells us that, given a constructor parameter of arbitrary type c , if we have a way of turning c into a (let us say f:c->a ) and c into b (let us say g:c->b ), then we can turn a c into a*b . The constructor, which is entirely determined by f and g , is called:

<f*g> : c -> a*b

It is quite intuitive to imagine that we are multiplying the two functions f and g into a new, über-function which takes as input a value of c , passes it to both f and g “in parallel”, and then returns the tuple of their results.

We can implement this in our categorical library:

export interface Prod<a,b> { fst:a, snd:b }

let times = <c,a,b>(f:(_:c) => a, g:(_:c) => b) =>

(x:c) : Prod<a,b> => ({ fst:f(x), snd:g(x) })

More interestingly, though, we can extend our original definition of function ( Fun ) so that it allows us to easily create such a parallel composition of functions that yields tuples:

export interface Fun<a,b> {

...

times:<c>(g:Fun<a,c>) => Fun<a, Prod<b,c>>

} export let fun = <a,b>(f:(_:a) => b) : Fun<a,b> => ({

...

times: function<c>(this:Fun<a,b>, g:Fun<a,c>) : Fun<a,Prod<b,c>>

{ return fun(times(this.f, g.f)) }

})

At this point, we can use times in order to generate a tuple from the functions which would generate its components:

let incr:Fun<number,number> = fun(x => x + 1)

let is_even:Fun<number,boolean> = fun(x => x % 2 == 0)

let f:Fun<number,Prod<number,boolean>> = incr.times(is_even)

Of course it must also be possible to destruct a tuple, that is to extract the values of its components in order to perform specific operations on them. We are thus required to have two functions, called the projections, with the following signature:

fst: a*b -> a

snd: a*b -> b

These two functions are implemented quite easily as:

export let fst = function <a,b>() : Fun<Prod<a,b>,a> {

return fun<Prod<a,b>,a>(p => p.fst) }

export let snd = function <a,b>() : Fun<Prod<a,b>,b> {

return fun<Prod<a,b>,b>(p => p.snd) }

We can use these functions in order to perform specific operations on elements of tuples, for example as:

let incr:Fun<number,number> = fun(x => x + 1)

let is_even:Fun<number,boolean> = fun(x => x % 2 == 0)

let f:Fun<number,Prod<number,boolean>> = incr.times(is_even)

let g:Fun<number,number> = f.then(fst()).then(incr)

When do we know that we have implemented our utility functions on tuples correctly? Category theory offers a powerful answer to this (which is rooted in a lengthy discussion about universal constructions, which we will not replicate here). Basically, constructing and destructing a tuple are inverse operations, which annul each other:

<f,g>; fst = f

<f,g>; snd = g

This makes a lot of sense: creating a tuple and then extracting its first element could have been shortened up as simply creating the first element. Notice that the equations above are not really meant to be used in practice (it would be very pointless to create a tuple to immediately throw it away by only using one of its elements), but only to define the required relationship between the constructs given. If our implementation always satisfies the equations above (and ours does: exercise for the reader!), then it is well-behaved. Category theory goes further and tell us that the implementation, when it satisfies the equations above, actually becomes a universal construction, meaning that we have built the smallest yet most expressive version of the datatype that we could possibly imagine. No appeal to experience, seniority, or intuition is here needed: we enjoy mathematical certainty over having found the objective best implementation.

As a final addition to our on tuples, consider two functions working on unrelated types:

f: a -> c

g: b -> d

It would make sense to apply these functions in parallel, but unfortunately we cannot do so with the constructor described before: the input types a and b are not the same!

We can thus combine the functions in a new form of product of functions, which transforms tuples into tuples by applying two functions in parallel over the arguments:

f*g: a*b -> c*d

This is a form of map of tuples, in that it transforms the content of a tuple but preserves its “tuple structure”. We can notice that this function is actually a special instance of our constructor, acting on the input tuple a*b , respectively extracting the first and second element and passing them to f on one hand and g on the other:

f*g = <(fst;f)*(snd;g)>

We can implement this in our library as

let times_par = function<a,b,c,d>(f:Fun<a,c>, g:Fun<b,d>) :

Fun<Prod<a,b>,Prod<c,d>> {

return (fst<a,b>().then(f)).times(snd<a,b>().then(g))

}

Of course this function can be integrated in our Fun interface as follows (be careful: b and c are swapped, given that inside Fun we are bound to have b as the result of the left-hand computation:

export interface Fun<a,b> {

...

map_times:<c,d>(g:Fun<c,d>) => Fun<Prod<a,c>, Prod<b,d>>

}

export let fun = <a,b>(f:(_:a) => b) : Fun<a,b> => ({

...

map_times: function<c,d>(this:Fun<a,b>, g:Fun<c,d>) : Fun<Prod<a,c>, Prod<b,d>> { return times_par(this, g) }

})

We could now use this in, for example, the following code:

let incr:Fun<number,number> = fun(x => x + 1)

let decr:Fun<number,number> = fun(x => x - 1)

let is_even:Fun<number,boolean> = fun(x => x % 2 == 0)

let f:Fun<number,Prod<number,boolean>> =

(incr.times(decr)).then(

incr.map_times(is_even))

Notice that incr.times(decr) takes a number (say x ) and produces a tuple of numbers: (x+1,x-1) . incr.map_times(is_even) takes as input a tuple of numbers (say (x,y) ) and produces a tuple of a number and a boolean (x+1,y is even) .

L et us close with one final note about the name, “product”. Tuples are called products because, if we see types as sets (not fully accurate, but close enough), then the number of possible elements of a tuple will be equal to the number of elements of the first set times the number of elements of the second set. For example, consider two types: A = {1,2,3} and B = {"a", "b"} . There are exactly six possible tuples where the first element has type A and the second has type B : (1,"a") , (1,"b") , etc.

Discriminated unions (sums)

The second fundamental interface is (discriminated) unions, which dictates how we can carry around polymorphic values. Polymorphic values can be of either one or another type, and are useful when we do not know exactly what for value we have, but we are sure that it belongs to a fixed set of possibilities (for example an API call might result in either an error, or a result). Discriminated unions are called, in the categorical context, sums.

A category supports sums if, for all pairs of types a and b , there exists a type a+b . This type, just like for tuples, must support a series of functions to construct, destruct, and transform its values.

Let us start with construction. Since we said that a+b either contains a value of a or a value of b , then it makes sense that we can construct a+b from either of those types:

inl : a -> a + b

inr : b -> a + b

For example, we could build Error+Person from inl(e) , where e is an Error , or from inr(p) , where p is a Person .

In our library, this becomes quite simply:

export type Sum<a,b> = { kind:"left", value:a } |

{ kind:"right", value:b } export let inl = function <a,b>() : Fun<a, Sum<a,b>> {

return fun<a, Sum<a,b>>(x => ({ kind:"left", value:x })) }

export let inr = function <a,b>() : Fun<b, Sum<a,b>> {

return fun<b, Sum<a,b>>(x => ({ kind:"right", value:x })) }

Suppose we wanted to “destruct” a Sum<a,b> into a value of type c . Of course we know that Sum<a,b> contains either an a , or a b . If we were able to provide a function f:a->c and a function from g:b->c , then we could check the content of our sum; if it is an a , then we invoke f , if it is a b , then we invoke g . This process of destruction is known as [f+g] , and is implemented quite directly as follows:

let plus = <c,a,b>(f:(_:a) => c, g:(_:b) => c) => (x:Sum<a,b>) => x.kind == "left" ? f(x.value) : g(x.value)

We can now integrate this into our Fun in order to provide an improved access interface:

export interface Fun<a,b> {

...

plus:<c>(g:Fun<c,b>) => Fun<Sum<a,c>, b>

} export let fun = <a,b>(f:(_:a) => b) : Fun<a,b> => ({

plus: function<c>(this:Fun<a,b>, g:Fun<c,b>) : Fun<Sum<a,c>, b> {

return fun(plus(this.f, g.f)) }

})

Thanks to this interface, we could build the following:

let is_even:Fun<number,boolean> = fun(x => x % 2 == 0)

let not:Fun<boolean,boolean> = fun(x => !x)

let f:Fun<Sum<number,boolean>,boolean> = is_even.plus(not)

f takes as input either a number, or a boolean. If a number comes in, then we check whether or not it is even, and return the resulting boolean; if a boolean comes in, then we negate it, and return the resulting boolean. In both cases we have produced a boolean.

Let us work on one final addition to our library on sums. Consider two apparently unrelated functions:

f : a -> b

g : c -> d

Suppose we would like to use them to destruct a sum a+c . Unfortunately, this is not always possible: b and d are not the same, and so we would end up with irreconcilable types. There is a workaround though: we could transform both sides of the sum in parallel, thereby obtaining the following function:

f+g : a+c -> b+d

This sort of transformation is akin to map in many languages. Fortunately, this operator is not really a new one, just a very convenient reformulation of the existing destructor, which invokes f in case of a , g in case of b , and then wraps the result in b+d by respectively invoking inl or inr :

f+g = [(f;inl)+(g;inr)]

This becomes the following code:

let plus_par = function<a,b,c,d>(f:Fun<a,b>, g:Fun<c,d>) :

Fun<Sum<a,c>,Sum<b,d>> {

return (f.then(inl<b,d>())).plus((g.then(inr<b,d>())))

}

Moreover, we can add this operator to our Fun interface as follows:

export interface Fun<a,b> {

...

map_plus:<c,d>(g:Fun<c,d>) => Fun<Sum<a,c>, Sum<b,d>>

} export let fun = <a,b>(f:(_:a) => b) : Fun<a,b> => ({

...

map_plus: function<c,d>(this:Fun<a,b>, g:Fun<c,d>) :

Fun<Sum<a,c>, Sum<b,d>> {

return plus_par(this, g) }

})

Thanks to this interface, we could build all sorts of combinations of functions into sums, such as the small example of:

let incr:Fun<number,number> = fun(x => x + 1)

let not:Fun<boolean,boolean> = fun(x => !x)

let f:Fun<Sum<number,boolean>,Sum<number,boolean>> =

incr.map_plus(not)

f takes as input either a number, or a boolean. The (left) number is incremented by one, whereas the (right) boolean is negated. The two resulting values must be kept separate given that they are different, therefore the output of f is Sum<number,boolean> .

A small note about the name. “Sums” are so-called because the number of elements of A+B is actually equal to the number of elements of A plus the number of elements of B . For example, consider two types: A = {1,2,3} and B = {"a", "b"} . There are exactly five possible sums where the first element has type A and the second has type B : inl(1) , inl(2) , inl(3) , inr(“a”) , and inr(“b”) .

Functions (exponentials)

The third and last fundamental interface is functions, also called exponentials. Fortunately, we have already seen most of it, thanks to the implementation of Fun<a,b> . Functions, just like all other types, are constructed, destructed, and transformed.

The construction of functions is a bit odd, as it presupposes a function to begin with. In this sense, it reminds transformation to an extent. Suppose we had a function f:a*b->c . This function takes as input a whole tuple as a parameter. We want to create (here we see the construction in action) a function which only takes a simpler type as input: instead of a tuple, a single a . This would lead us to a function which takes as input an a and returns a new function as output: b->c . We “prefer” this formulation as it only deals in “simpler” types ( a*b is no simple type), which are primitive types and functions. This operator is called curry :

curry(f) : a -> (b -> c)

Notice how the strategic placement of the brackets suggests that curry(f) takes as input an a and returns as output the function that further accepts b as input (the extra parameter which still needs to be specified) and then presumably invokes f with both the a and the b in order to produce the final c .

We implement this trivially as:

export let curry = function<a,b,c>(f:Fun<Prod<a,b>,c>) :

Fun<a,Fun<b,c>> {

return fun(a => fun(b => f.f({ fst:a, snd:b }))) }

Suppose we had a function f:a->b and a value x:a . We could invoke f(x) , thereby destroying the function and obtaining a value of the simpler type b . This mechanism is called eval or apply (we go with apply : eval is a JavaScript intrinsic operator, and trying to redefine it would be very bad practice), and is specified as:

apply : (a -> b) * a -> b

This operator is also implemented trivially in two handy equivalent forms as:

export let apply = <a,b>(f:Fun<a,b>, x:a) : b => f.f(x)

export let apply_pair = function<a,b>() :

Fun<Prod<Fun<a,b>, a>,b> {

return fun(p => p.fst.f(p.snd)) }

Suppose now we had a function f:a->b , and two functions g:a'->a and h:b->b' . We could then transform f into a new function a'->b' by simply making a “sandwich” around it:

g;f;h

Fortunately, this transformation comes for free with the then and after operators:

f.then(h).after(g)

would implement the transformation we have just seen. Note that the direction of the transformations is a bit peculiar. f is given, and so are a and b . Then h goes further in the transformation, from the given b into b' . On the other hand, g goes in the opposite direction: it does not go from a given type, but into a given type. This opposite switch in direction when dealing with transformations of arrow inputs versus outputs is called covariance (“in the same direction”, like h ) and contravariance (“in the opposite direction”, like g ).

A small note about the name. Functions are also called exponential objects, that is a function a->b is written b^a ( b to the power of a ). Suppose that both a and b were sets. An interesting question then becomes “how many functions exist from a to b ?” Consider that a function a->b associates an element of b to each possible input of a . Suppose thus that a={a1,a2,...,an} and b={b1,b2,...,bm} . Then one function a->b could become (seen as a set of input/output tuples) {(a1,b1),(a2,b1),...,(an,b1)} . Another function could become {(a1,b2),(a2,b2),...,(an,bn)} . In general, we could enumerate all functions by taking all sequences of n values from b . This would lead us to m^n combinations. Hence the name “exponential”.

Bi-Cartesian Closed Categories

Categories where all objects can be combined into products, sums, or exponentials, are called Bi-Cartesian Closed Categories (BCCC). Such categories must also have a little bit more structure, specifically the existance of a trivial type called the terminal object, or Unit (in Category theory often called 1 ), and of another trivial type called the initial object, or Zero (in Category theory often called 0 ). Given any type a , there must exist two arrows:

absurd: Zero -> a

unit: a -> Unit

The Zero type has no inhabitants. It can never be instantiated. For this reason the function absurd: Zero -> a always exists: since no instance of Zero exists, it cannot be called!

The Unit type contains a single element. unit: a -> Unit is slightly more interesting, as it allows us to “ignore” a value of an arbitrary type a . These types and their functions are implemented as:

export type Zero = never

export let absurd = <a>() : Fun<Zero,a> => fun<Zero,a>(_ => { throw "Does not exist." }) export interface Unit {}

export let unit = <a>() : Fun<a,Unit> => fun(x => ({}))

Moreover (actually, this is true of any category), given any type a , there exists a trivial identity function id : a -> a . This is implemented as:

export let id = function<a>() : Fun<a,a> { return fun(x => x) }

We will now focus on uses of BCCC’s in order to first define some data conversions which are always true, and then to define some advanced data types for modeling abstract properties such as exception handling and processes.

Equivalences

The most interesting aspect of BCCC’s is that they guarantee the existence of a series of equivalences, which we are quite familiar with from school algebra, and which come back in a very abstract form as part of a standard library. For example, we know that, when dealing with numbers, a*b+a*c=a*(b+c) . Amazingly enough, this is also true in BCCC’s, thus when a , b , and c are all types. It is easy to understate the power of this fact, especially since the equivalence seems so trivial. The presence of such equivalences over generic data structures gives us powerful hints as to what is always true, and thus can belong to a standard library, and what is not, and thus is domain specific. Moreover, by confirming well-known equivalences, instead of having to reason in terms of strange constructs such as adapters, factories, or dependency-injections, we can reuse our familiar knowledge at a higher level of abstraction. This leads us to a lot of expressive power, without requiring any effort of the imagination. Moreover, each of these equivalences offers us an insight in lossless transformations of data, which are the core of the sort of data processing which is often done to glue different libraries together.

Swapping the elements of a tuple does not change the amount of information stored in it, merely the format. Thus, this becomes the first equivalence we focus on: a*b=b*a . This simply requires a swap function which turns the elements around. We can achieve this very easily by:

swap_* = <snd*fst>

which turns into code quite literally:

export let swap_prod = function<a,b>() : Fun<Prod<a,b>,Prod<b,a>> {

return snd<a,b>().times(fst<a,b>())

}

Along the very same line of reasoning (changing the order does not destroy, nor introduce, information), we can also show that a+b=b+a . This is another swap function which, when it finds an a puts it to the right with inr and when it finds a b puts it to the left with inl :

swap_+ = [inr+inl]

or, in code:

export let swap_sum = function<a,b>() : Fun<Sum<a,b>,Sum<b,a>> {

return inr<b,a>().plus(inl<b,a>()) }

Consider now the following datatype: a*b+a*c . This datatype contains either a*b , or a*c . This means that in both cases there will certainly be an a , accompanied by either a b or a c . The equivalence this leads us to is thus a*b+a*c=a*(b+c) . Such an equivalence requires the existence of two arrows, in both directions, and which composed together form an identity. Let us start with the first direction, which simply ignores the unnecessary information on each side of the tuple:

i : a*b+a*c -> a*(b+c)

i = <f*g>, where f:a*b+a*c->a and g:a*b+a*c->(b+c)

f = [fst+fst]

g = [(snd;inl)+(snd;inr)]

The opposite direction is a bit trickier, since it requires us to manually emulate the closure of a when destructing the sum:

j : a*(b+c) -> a*b+a*c

j = (id*[f+g]);swap;apply, where f:b->a->a*b+a*c and g:c->a->a*b+a*c

f = curry(f1) where f1:b*a->a*b+a*c

f1 = swap;inl

g = curry(g1) where f1:c*a->a*b+a*c

g1 = swap;inr

The implementation is, once again, just a faithful transliteration of the above definitions, plus a bit of extra verbosity to account for TypeScript being less expressive than mathematical language (or mathematically-inspired dialects such as Haskell):

export let distribute_sum_prod = function<a,b,c>() : Fun<Prod<a, Sum<b,c>>, Sum<Prod<a,b>, Prod<a,c>>> {

let f1:Fun<Prod<b,a>,Sum<Prod<a,b>, Prod<a,c>>> =

swap_prod<b,a>().then(inl())

let f = curry(f1)

let g1:Fun<Prod<c,a>,Sum<Prod<a,b>, Prod<a,c>>> =

swap_prod<c,a>().then(inr())

let g = curry(g1)

let j = id<a>().map_times(f.plus(g)).then(

swap_prod()).then(

apply_pair())

return j

} export let distribute_sum_prod_inv = function<a,b,c>() : Fun<Sum<Prod<a,b>, Prod<a,c>>, Prod<a, Sum<b,c>>> {

return fst<a,b>().times(inl<b,c>().after(snd<a,b>())).plus(

fst<a,c>().times(inr<b,c>().after(snd<a,c>())))

}

We also known that 0 and 1 exhibit special behavior when used as exponents, multiplied, or added to. Consider that, for example, a function a^0 could never be invoked, seen that 0 has no instances. For this reason, we can always go from 0->a into 1 , given that neither of them contains any meaningful information:

0->a = 1

The implementation is quite trivial, since we must only ignore the input function (which cannot be invoked anyway!), and this is precisely what unit does:

i : (0->a) -> 1

i = unit

As usual, the code is just a faithful translation:

export let power_of_zero = function<a>() : Fun<Fun<Zero, a>, Unit> {

return unit()

}

The opposite is slightly more articulated. It requires a common “trick” which we have already seen in action when converting a*(b+c)->a*b+a*c : reformulating the problem by grouping all available inputs, in our case 1 and 0 , in a large tuple, and then currying in order to introduce -> wherever needed:

i : 1 -> (0 -> a)

i = curry(f), where f: 1*0 -> a

f = snd; absurd

In code, this becomes:

export let power_of_zero_inv = function<a>() :

Fun<Unit, Fun<Zero, a>> {

return curry(absurd<a>().after(snd<Unit,Zero>()))

}

We know that 1 carries no real information. For this reason, if we get a tuple which contains a 1 somewhere, we can discard the 1 and keep the amount of information constant. This can be restated as a*1=a=1*a . Conversions between a*1 and a are simple. The forward conversion is no more than fst . The second conversion simply introduces a 1 by using the unit function, which can always be invoked since we can always “conjure” a 1 out of thin air. In code:

export let product_identity = function<a>() : Fun<Prod<a,Unit>,a> {

return fst<a,Unit>()

} export let product_identity_inv = function<a>() :

Fun<a,Prod<a,Unit>> {

return id<a>().times(unit<a>())

}

Conversions between a+0 and a are slightly less intuitive. Keep in mind that there is no way to instantiate 0 , so if we ever were to obtain an a+0 , then there would be no way for it to have been generated from 0 . Thus, we are certain that it had been generated from a . Recall that absurd<a>:Zero->a is the “uncallable” function that pretends to be able to produce an arbitrary a . absurd is uncallable because there exists no instance of Zero , therefore we cannot instantiate the argument to pass it, but once again this constitutes no real danger since a+0 was certainly not created from 0 . Thanks to absurd , we can define the forward conversion:

i : a+0->a

i = [id+absurd]

The other direction is trivial, as it simply encapsulates a value of a into an appropriate sum:

i : a->a+0

i = inl

In code:

export let sum_identity = function<a>() : Fun<Sum<a,Zero>,a> {

return id<a>().plus(absurd<a>())

} export let sum_identity_inv = function<a>() : Fun<a,Sum<a,Zero>> {

return inl<a,Zero>()

}

An especially insight-giving equivalence is a*a = a^(1+1) . Not only is it trivially true, but the type 1+1 (also, unsurprisingly, called 2 ) has a special meaning. 1+1 contains only two values: inl applied to 1 , and inl applied to 1 . If we assume the convention that the first value is False , while the second value is True , then we have defined a categorical representation for Bool . Now, a function which takes as input 2 (a boolean), and returns an a actually is not much different from just two values of type a : one for False , the other for True .

The forward direction uses the typical trick of grouping together all the inputs, that is (a*a)*(1+1) , determining the final result with them (a simple a ), and finally currying in order to achieve the right number of arrows:

i : a*a -> ((1+1)->a)

i = curry(j), where j : (a*a)*(1+1) -> a

j = distrib_+_*; j1, where j1:((a*a)*1)+((a*a)*1) -> a

j1 = [(fst;fst)+(fst;snd)]

More complex equivalences arise between functions. Specifically, consider a function a+b->c that takes as input a sum, and returns a value. This function is actually equivalent to two functions that independently work on a and b and return a c :

The forward direction is built by splitting the function in two partial functions, each invoking the original with the argument encapsulated respectively in inl and inr :

i : ((a+b)->c)->((a->c)*(b->c))

i = <f*g>, where f:((a+b)->c)->(a->c) and g:((a+b)->c)->(b->c)

f = curry(f1), where f1:((a+b)->c)*a->c

f1 = id*inl; apply

g = curry(g1), where g1:((a+b)->c)*b->c

g1 = id*inr; apply

The backward direction is more complex, and makes use of both our usual trick of putting all inputs in a large tuple and currying later, and also requires the use of the distributive property of sum over product that we defined before:

j : ((a->c)*(b->c))->((a+b)->c)

j = curry(j1;j2;j3), where

j1 : ((a->c)*(b->c))*(a+b)->((a->c)*(b->c))*a)+((a->c)*(b->c))*b),

j2 : ((a->c)*(b->c))*a)+((a->c)*(b->c))*b) ->

((a->c)*a)+((b->c)*b),

j3 : ((a->c)*a)+((b->c)*b)->c

j1 = distribute_sum_prod

j2 = (fst*id)+(snd*id)

j3 = [apply+apply]

We omit the code from now on, as it is now just a (lengthy) translation of the definitions above and therefore adds nothing (but verbosity).

Functions can be curried, but also “uncurried”. This means that we can state the equivalence between a*b->c = a->b->c . The forward direction is no more than just curry . The backward direction requires the now almost trite trick of grouping all arguments together, that is the input function and the input pair, together in a single large tuple (a->(b->c))*(a*b) , and then applying twice:

i : (a->b->c) -> ((a*b)->c)

i = curry(j), where j: (a->(b->c))*(a*b) -> c

j = conv;j1, where j1:((a->(b->c))*a)*b -> c,

conv:((a->(b->c))*(a*b)) -> (((a->(b->c))*a)*b)

conv = <<fst*(snd;fst)>*(snd;snd)>

j1 = apply * id; apply

The last equivalence suggests that the order of a functions arguments can be swapped. Indeed, c^b^a = c^a^b , and because of symmetry we only need one direction. The usual trick applies here: we define a large tuple containing all input data (thus the function and its input), perform a single application on the swapped input argument, and then curry in order to introduce the right number of arrows:

i : (a->(b->c)) -> (b->(a->c))

i = curry(j);uncurry, where j:(a->b->c)*(b*a)->c

j = j1;j2;j3, where j1:((a->b->c)*(b*a))->((a->b->c)*(a*b))

j2:((a->b->c)*(a*b))->(((a*b)->c)*(a*b))

j3:(((a*b)->c)*(a*b))->c

j1 = id*swap_prod

j2 = uncurry*id

j3 = apply

Conclusion

In this article we have discussed a practical introduction to Category theory and Bi-Cartesian Closed Categories. We have defined categories, and provided an implementation for functions that mirrors the categorical definition. We have then seen how to augment our definitions with a series of expressive data combinators for polymorphism, tuples, and higher order functions. These combinators also required adding new ways to compose functions together in order to achieve parallel invocation of functions, pattern matching/visiting, and more.

Based on these combinators we have built a standard library of conversions. Thanks to this standard library it is possible to create adapters that change the format of a datatype, without affecting the amount of information contained inside it.

The library can be used to elegantly implement many advanced constructs. For example, the well-known Option<A> datatype ( Maybe in Haskell, Option in Rust, etc.) is no more than Sum<Unit,A> (or 1+a in Category Theory, yet more elegantly!). Even more interestingly, the implementation of monads also benefits from this BCCC treatment: complex monads such as State , Parser , or Coroutine all become shorter and cleaner when built with BCCC combinators and equivalences. An example application is building parsers, type checkers, and interpreters. A discussion of the implementation of these advanced patterns will be deferred to a later article, but the curious reader may already check out the repository and its npm package in order to take a look at the code.

About the author

A side-note: if you like the topics discussed in this article, we are always looking for like-minded people who want to participate in the development of these libraries. Especially if you live anywhere near Rotterdam (and if you do not, come take a look at the coolest city ever!), drop us a line and let’s have a chat. Moreover, all we discussed in this article is used in practice: at Hoppinger we build all sorts of cool applications with these techniques, and more.