Let’s think about things that can be put together.
Here’s two lists, combined into one big list.
list :: [Int]
= [1,2,3] ++ [4,5,6]
list -- list == [1,2,3,4,5,6]
Great!
What about two strings combined into one large excellent string?
string :: String
= "Great" ++ " Stuff"
string -- string == "Great Stuff"
Sure! Seems great. String
is actually a List
of Char
so really that’s just the same thing happening twice there. So what’s Semigroup
got to do with this? Semigroup
is the generalisation of combining things together. How is it defined?
Definition
Prelude> :i Semigroup
…gets us…
class Semigroup a where
(<>) :: a -> a -> a
{-# MINIMAL (<>) #-}
So by the looks we can just define it with the <>
function.
Therefore we could write our string concat function as "Great" <> "Job"
or combine lists like [1,2,3] <> [4,5,6]
.
It doesn’t just involve putting sets of things together.
Maybe Semigroup
Let’s define our own datatype MyMaybe
which you may notice is very similar to Maybe
. The Semigroup
instance can be used to combine two of them.
data MyMaybe a = Yeah a | Nope
What’s interesting about this one is that by insisting that the value inside is also a Semigroup
, we can do some exciting multi-level combining.
instance (Semigroup a) => Semigroup (MyMaybe a) where
Yeah a) <> (Yeah b) = Yeah (a <> b)
(<> Nope = a
a Nope <> b = b
Let’s see it at work.
nah :: MyMaybe String
= Nope <> Nope
nah -- nah == Nope
Sure.
first :: MyMaybe String
= Yeah "Totally" <> Nope
first -- first == Yeah "Totally"
Plausible.
second :: MyMaybe String
= Nope <> Yeah "Great"
second -- second == Yeah "Great"
OK.
And the more interesting one…
both :: MyMaybe String
= Yeah "Totally" <> Yeah "Great"
both -- both = Yeah "TotallyGreat"
What happened here? We combined two MyMaybe
values AND the String
values inside them as well, without really putting much effort in whatsoever. Great job, Semigroup
.
Sum Semigroup
We can also use this pattern to describe combining numbers. Integers, for instance, can form several Monoids. One is addition. Let’s build a newtype so that we can make a Semigroup
instance.
newtype MySum a = MySum {
getMySum :: a
}
Here is a Semigroup
instance, it uses pattern matching to take the original values out, adds them together, them wraps them in another MySum
instance.
instance (Num a) => Semigroup (MySum a) where
MySum a <> MySum b = MySum (a + b)
We can create several MySum
instances, and then combine them together with <>
.
ten :: MySum Int
= MySum 1 <> MySum 7 <> MySum 2
ten -- ten == MySum 10
Or we could use the getMySum
function in the newtype to unwrap it.
anotherTen :: Int
= getMySum $ MySum 1 <> MySum 7 <> MySum 2
anotherTen -- anotherTen == 10
(This is obviously quite an overwrought way to add 3 numbers together)
Product Semigroup
We could also combine numbers by multiplying them together.
newtype MyProd a = MyProd {
getMyProd :: a
}
instance (Num a) => Semigroup (MyProd a) where
MyProd a <> MyProd b = MyProd (a * b)
sixtySix :: Prod Int
= MyProd 11 <> MyProd 2 <> MyProd 3
sixtySix -- sixtySix = Prod 66
anotherSixtySix :: Int
= getMyProd $ MyProd 11 <> MyProd 2 <> MyProd 3
anotherSixtySix -- anotherSixtySix = 66
So, yes.
Hopefully you get the idea of what’s going on here. A Semigroup
is a nice way of describing things we can smash together, and it becomes even better when we extend it into Monoid
, that gets us a load of Foldable
stuff for free. We’ll come to that soon…
Make sense? If not, why not get in touch?
Further reading: