Ensure failsafe combination using monoids
This is Tutorial 8 in the series Make the leap from JavaScript to PureScript. Be sure* to read the series introduction where I cover the goals & outline, and the installation, compilation, & running of PureScript. I will be publishing a new tutorial approximately once-per-month. So come back often, there is a lot more to come!
Index | << Introduction < Tutorial 7 | Tutorial 9 > Tutorial 27 >>
Welcome to Tutorial 8 in the series Make the leap from Javascript to PureScript and I hope you’re enjoying it thus far. In this tutorial, we define and promote our semigroups from Tutorial 7 to monoids. Be sure to read the series Introduction to learn how to install and run PureScript. I borrowed (with permission) the outline and javascript code samples from the egghead.io course Professor Frisby Introduces Composable Functional JavaScript by Brian Lonsdorf — thank you, Brian! A fundamental assumption is that you have watched his video before tackling the equivalent PureScript abstraction featured in this tutorial. Brian covers the featured concepts extremely well, and it’s better that you understand its implementation in the comfort of JavaScript.
The markdown and all code examples for this tutorial are located on Github. If you read something that you feel could be explained better, or a code example that needs refactoring, then please let me know via a comment or send me a pull request. Finally, If you are enjoying this series, then please help me to tell others by recommending this article and favoring it on social media. My Twitter handle is @adkelley
Semigroups — you deserve a promotion!
Recall from Tutorial 6 and Tutorial 7 that a semigroup is a type that has an append method. For example, we covered the Additive
, & Conj
semigroups and their append methods are addition and logical conjunction respectively. The First
semigroup was even more interesting because its append method returns the first non-Nothing value.
Let’s look at Additive
from the module Data.Monoid.Additive. The append method is an addition, where:
Additive x <> Additive y == Additive (x + y)
You may recall that we used it in Tutorial 7 to help merge Nico’s game accounts by adding her points
from each account.
There is another interesting property with Additive
that manifests itself when you add an element to zero. If we have Additive (1 + 0), we get back (Additive 1), Additive (2 + 0) returns (Additive 2), and so on. Thus anything plus zero will return that thing. Here, zero is the called the identity or neutral element for Additive.
Meaning whenever we append one or more numbers with zero, we get back their sum. You'll see why this is important shortly. But first, let's introduce the identity elements for the other semigroups covered in Tutorial 7.
Determining the identity element
Consider Conj
from Data.Monoid.Conj, whose append method is logical conjunction:
Conj x <> Conj y == Conj (x && y)
We used it in Tutorial 7 to append the hasPaid
values (either true or false) from Nico’s three accounts. So what is Conj
's identity element? If we have:
true && true == true
false && false == false
true && false == false
false && true == false
So it appears that true
is the identity element or neutral value for Conj
.
Finally, let’s look at First
from Data.Maybe.First, whose append operation returns the first (left-most) non-Nothing value. We used First
in Tutorial 7 to take the first non-nothing name from Nico’s three accounts. For example, Nothing <> (Just Nico)
is (Just Nico)
and (Just Nico) <> Nothing
is Just Nico
. So it appears that the identity element for First
is Nothing
.
In PureScript, mempty
refers to the identity element, like the 0
, true
, and Nothing
values we've seen so far. You can find the mempty
declaration for each semigroup in their respective modules, e.g., Data.Monoid.Additive
. So
Additive x <> Additive y == Additive (x + y)
mempty :: Additive _ == Additive zero Conj x <> Conj y == Conj (x && y)
mempty :: Conj _ == Conj true
Now, you might have wondered from the last tutorial, “why doesn’t PureScript name these modules Data.Semigroup.XX
?" Well, that is the main subject of this tutorial - in addition to being semigroups, Additive
, Conj
, and First
are also monoids because they have an identity element.
An informal definition of a monoid
Monoids are semigroups with an identity element. That’s it — really! That is all you need to know from a programmers perspective. Like semigroups, monoids come from abstract algebra. Now I said it in Tutorial 6, and I’ll say it here again — don’t let this, or any other mathematical name, scare you from learning functional programming. If we keep the names (e.g., monoid), then we benefit from a whole lot of information derived from the mathematics, namely the types and their laws!
For a more formal definition of this and other algebraic terms, one of the most approachable tutorial series for functional programmers is from Bartosz Milewski. So, after you’ve run through my code examples below, and made some of your own, I encourage you to carve out some time each week to explore what he has to say on these topics.
Not every semigroup gets a promotion
Are there semigroups that don’t have an identity element? You bet! In Tutorial 6, I used my own, custom implementation of First
, instead of Data.Maybe.First. My First
semigroup in Tutorial 6 ignored every element within an append operation, except for the first argument, returning that element regardless. But what if the first argument is an empty array or list? Well, that will just blow up in our faces because we don't have any value to return as our first argument.
If you are skeptical (and you should be), then go back to the code in Tutorial 6 and add the following to the main
function. It's an attempt to log the result of appending an empty array with a non-empty one. You'll find that it won't even compile.
logShow $ (First null) <> (First [1])
So, sadly, First
from Tutorial 6 stays a semigroup because it doesn't have an identity element. The upside is, in contrast to JavaScript, that you are finding this out at compile time instead of run time where the stakes are much higher.
But, what about First
from the module Data.Maybe.First
? You may recall I used this version in Tutorial 7. You will find that the code below compiles and returns (First (Just [1]))
.
logShow $ (First Nothing) <> (First (Just [1]))
Because this implementation of First
has the identity element Nothing
, it just earned its monoid badge - congratulations First
!
Monoid usage and examples
Monoids are useful because you can perform ‘safe’ operations with them. For example, let’s say our code returns an empty list of the Additive
type. Then, instead of blowing up, the value returned will be zero. In summary, for the three monoids we've covered so far, mempty
returns the following:
logShow (mempty :: Additive Int) -- (Additive 0)
logShow (mempty :: Conj Boolean) -- (Conj true)
logShow (mempty :: First Int) -- First (Nothing)
Note the inline type assignment (e.g., mempty :: Additive Int
) within our PureScript code. It is a handy feature for declaring the type of generic type constructs such as mempty
.
Code examples
We’ll keep the code light and easy in this tutorial; giving a few examples of appending the identity element (i.e., mempty
) to the monoids Additive
, Conj
, and First
. In the next tutorial, we'll add several more monoids to our tool belt and give examples of how you can take advantage of them in your code.
-- logs (Additive 6)
logShow $ foldr (<>) mempty $ map Additive [1, 2, 3]-- logs (Conj false)
logShow $ foldr (<>) mempty $ Conj <$> [true, true, false] -- logs First ((Just 1))
logShow $ foldr (<>) mempty $ First <$> [Nothing, Just 1, Just 2]
Using map to construct semigroups
In the examples above, to keep things ‘DRY’ (i.e., Don’t Repeat Yourself), I am leveraging our old friend map
to take an array of concrete types (e.g., Int, Number, etc.) and turn them into an array of semigroups before folding them. In the second and third examples, I’m using map’s infix operator <$>
. You’ll see <$>
often, so it is important you are aware of this idiomatic usage. In the next tutorial, I’ll show you how to shorten these expressions even further.
If we don't use map
then the first example above would look like this:
-- (Additive 6)
logShow $ foldr (<>) mempty [Additive 1, Additive 2, Additive 3]
So map
is perfect for eliminating this repeated pattern. Why does this work? Because Additive
, Conj
, and First
are type constructors, and therefore they are functions (a -> b)
, just like the Either
and Maybe
constructors we have seen before. Type constructors are new types that we compose from old ones. For example, Int
becomes Additive Int
.
As shown below, we can use a type constructor as our first argument (a -> b)
in the map expression, with the second argument being a foldable structure f
of elements a
, and returning the same structure f
of new elements b
.
map :: forall a b. (a -> b) -> f a -> f b
Here is a concrete declaration of map
using Array
with the Additive
monoid:
map :: (Int -> Additive Int) -> Array Int -> Array (Additive Int)
Summary
In this tutorial, we learned that semigroups could be promoted to monoids if they have an identity element. An identity element in PureScript is the value mempty
, where
forall x. mempty <> x = x <> mempty = x
The above means that if you append a monoid with its identity element, then you get back the monoid with that element. The value of the identity element, mempty
will depend on the monoid. For example, in the case of the Additive
monoid, the value of mempty
is zero.
In the next tutorial, we will look at several new monoids and how to advantage of them in our code. Finally, if you are enjoying this series, then please help me to tell others by recommending this article and favoring it on social media. My twitter handle is @adkelley. Till then!