Functional Programming: Using the ‘Semigroup’ Type Class to Merge Things
Tags: Programming, JavaScript, Functional Programming, Software Development, Typescript

The ‘Semigroup’ Type Class

First we look at what a Semigroup is in general and what it does. Please read the following Gist:

That was a lot! But no worries — we’ll talk about everything.

The first thing we do is define the Semigroup type class with a TypeScript interface. As you can see, for something to be an instance of the Semigroup type class, it needs to have a concat method defined.

This concat method takes in two parameters of the same type and returns a single value of this type. So we can see that this concat method combines/merges/concatenates two values of type A into a single value of type A — e.g., it takes two strings and returns another string.

I don’t know about you, but I think that sounds very useful for a lot of things. We’ll see a more interesting example later on in this article.

Implementing a ‘Semigroup’ instance

What happens next in the Gist above is we implement a Semigroup instance for the number type under multiplication, which means we create an object with a concat method that takes in two numbers and multiplies them. We could also do this for addition. The only difference would be that instead of multiplying in the concat method, we add the two numbers together.

We also define a Semigroup instance for the type string . Here we add (concatenate) the two strings we pass into the concat method together to build up a new string.

The previous operations we executed in the concat methods were all associative , which means we don’t have to parenthesize the expression and can just write x * y * z . Associativity is a law that must hold for the concat method of a Semigroup instance.

Other ways of implementing a ‘Semigroup’ instance

Associativity can become a problem. Sometimes there’s no associative operation available for a type. Gladly, there are two other ways of creating a Semigroup instance for a type.

One way is to create a Semigroup instance with a concat method that just returns the first or last input. Take a look at the getFirstSemigroup and getLastSemigroup functions in the Gist.

With that getFirst and getLast logic, we could also easily create a Semigroup instance from a type that already has an Ord instance defined. We could do that by just returning the minimum or maximum values in the concat method.

The second way is to create a so-called free Semigroup of type A . That means we create a Semigroup instance for a type A by wrapping the value inside a singleton list, [A] , and then creating a Semigroup instance for the type Array<A> , where concat is (x, y) => x.concat(y) . x.concat refers to the built-in JS array method.

Implementing a ‘Semigroup’ instance for ‘Option<A>’

Merging two numbers or strings together is pretty easy, but what if we’d like to merge values that are wrapped inside a context like Option<A> ? We need some special logic for that:

Implementing a Semigroup instance for something like Option<A> is also not that hard.

We just had to define some extra logic in the concat method. Here, we did nothing more than to check if both values in our concat method are not none , and if so, we take out the value from our Option<A> and apply it to the concat method from whatever Semigroup instance we passed into getApplySemigroup .

Using ‘fold’ to merge an array of values

Now we have a feeling for what Semigroup s are and know that we can merge/combine/concatenate two values together. But sometimes we’d like to merge more than two values together. How can we accomplish this?

Luckily, there’s a function that does just that. It is called fold , and it’s very similar to the built-in JS array method reduce . Let’s take a look at it:

So as we can see, fold takes in a Semigroup<A> and returns a function that takes in an initial value A and an array of A ( Array<A> ) and returns an A . So it folds/reduces an Array<A> into a single value A . Under the hood, fold also uses Array.prototype.reduce (the reducer is the concat method from the passed in Semigroup instance).

That’s cool! We can now map over an array and merge/reduce it to a single value. This will be even cooler when we get to see a more interesting example later on.

Implementing a ‘Semigroup’ instance for an object

We’ve seen a few examples of how we can create a Semigroup instance for some types, but we haven’t seen how we can implement a Semigroup instance for an object . So let’s do it:

The first definition of semigroupSong was created with a more manual way.

We create a Semigroup instance for our Song type by defining a concat method that takes in two objects of type Song and combines them together with the rules/functions we defined for each key.

For example, we merge the key releaseYear by keeping the most recent year by filtering for the larger number.

We’ve written the rules/functions that merge the keys together ourselves, but we also could’ve used the concat methods from existing instances of Semigroup . Like our semigroupProduct , whose concat method merges the two values together by multiplying them.

The second definition of semigroupSong was created by using some functions from fp-ts . It follows roughly the same logic as before, but this time we don’t define a concat method and just define/use complete Semigroup instances for each key of the object instead of just a function.

Under the hood, it also just applies the inputs to the concat method of the Semigroup instance we defined for the key of the object.

The interesting thing about this example is we can now merge objects with custom merging logic for each key. This could be really useful — e.g., when we have two Song s from different data sources and want to combine/merge them into one but with some rules for what value should be kept for each key.

The Semigroup type class can be really helpful to define custom merging logic for types.