When looking at Data.Monoid
, I see there are various newtype
wrappers, such as All
, Sum
, or Product
, which encode various kinds of monoids. However, when trying to use those wrappers, I can't help but wonder what's the benefit over using their non-Data.Monoid
counterparts. For instance, compare the rather cumbersome summation
print $ getSum $ mconcat [ Sum 33, Sum 2, Sum 55 ]
vs. the more succinct idiomatic variant
print $ sum [ 33, 2, 55 ]
But what's the point? Is there any practical value having all those newtype
wrappers? Are there more convincing examples of Monoid
newtype
wrapper usage than the one above?
The point to it is that when you tag an Int as Product , you express your intent for the integers to be multiplied. And by tagging them as Sum , to be added together. Then you can use the same mconcat on both.
One answer is that thinking in terms of monoids can be very beneficial to parallelism and efficient data structures. Using the Monoid interface in Haskell allows you to leverage the many convenient functions that work with them.
As we can see, Monoid in Haskell is a typeclass that has three methods: mempty , mappend , and mconcat . mempty is a value that doesn't impact the result when used together with the binary operation. In other words, it's the identity element of the monoid. mappend (or <> ) is a function that puts two monoids together.
Monoids are great to wrap an existing data type in a new type to tell the compiler what operation you want to do.
Since they're newtypes, they don't take any additional space and applying Sum
or getSum
is a no-op.
There's more than one way to generalise foldr (see this very good question for the most general fold, and this question if you like the tree examples below but want to see a most general fold for trees).
One useful way (not the most general way, but definitely useful) is to say something's foldable if you can combine its elements into one with a binary operation and a start/identity element. That's the point of the Foldable
typeclass.
Instead of explicitly passing in a binary operation and start element, Foldable
just asks that the element data type is an instance of Monoid.
At first sight this seems frustrating because we can only use one binary operation per data type - but should we use (+)
and 0
for Int
and take sums but never products, or the other way round? Perhaps should we use ((+),0)
for Int
and (*),1
for Integer
and convert when we want the other operation? Wouldn't that waste a lot of precious processor cycles?
All we need to do is tag with Sum
if we want to add, tag with Product
if we want to multiply, or even tag with a hand-rolled newtype if we want to do something different.
Let's fold some trees! We'll need
fold :: (Foldable t, Monoid m) => t m -> m
-- if the element type is already a monoid
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> m
-- if you need to map a function onto the elements first
The DeriveFunctor
and DeriveFoldable
extensions ({-# LANGUAGE DeriveFunctor, DeriveFoldable #-}
) are great if you want to map over and fold up your own ADT without writing the tedious instances yourself.
import Data.Monoid
import Data.Foldable
import Data.Tree
import Data.Tree.Pretty -- from the pretty-tree package
see :: Show a => Tree a -> IO ()
see = putStrLn.drawVerticalTree.fmap show
numTree :: Num a => Tree a
numTree = Node 3 [Node 2 [],Node 5 [Node 2 [],Node 1 []],Node 10 []]
familyTree = Node " Grandmama " [Node " Uncle Fester " [Node " Cousin It " []],
Node " Gomez - Morticia " [Node " Wednesday " [],
Node " Pugsley " []]]
Strings are already a monoid using (++)
and []
, so we can fold
with them, but numbers aren't, so we'll tag them using foldMap
.
ghci> see familyTree
" Grandmama "
|
----------------------
/ \
" Uncle Fester " " Gomez - Morticia "
| |
" Cousin It " -------------
/ \
" Wednesday " " Pugsley "
ghci> fold familyTree
" Grandmama Uncle Fester Cousin It Gomez - Morticia Wednesday Pugsley "
ghci> see numTree
3
|
--------
/ | \
2 5 10
|
--
/ \
2 1
ghci> getSum $ foldMap Sum numTree
23
ghci> getProduct $ foldMap Product numTree
600
ghci> getAll $ foldMap (All.(<= 10)) numTree
True
ghci> getAny $ foldMap (Any.(> 50)) numTree
False
But what if we wanted to find the maximum element? We can define our own monoids. I'm not sure why Max
(and Min
) aren't in. Maybe it's because no-one likes thinking about Int
being bounded or they just don't like an identity element that's based on an implementation detail. In any case here it is:
newtype Max a = Max {getMax :: a}
instance (Ord a,Bounded a) => Monoid (Max a) where
mempty = Max minBound
mappend (Max a) (Max b) = Max $ if a >= b then a else b
ghci> getMax $ foldMap Max numTree :: Int -- Int to get Bounded instance
10
We can use newtype Monoid wrappers to tell the compiler which way to combine things in pairs.
The tags do nothing, but show what combining function to use.
It's like passing the functions in as an implicit parameter rather than an explicit one (because that's kind of what a type class does anyway).
How about in an instance like this:
myData :: [(Sum Integer, Product Double)]
myData = zip (map Sum [1..100]) (map Product [0.01,0.02..])
main = print $ mconcat myData
Or without the newtype wrapper and the Monoid
instance:
myData :: [(Integer, Double)]
myData = zip [1..100] [0.01,0.02..]
main = print $ foldr (\(i, d) (accI, accD) -> (i + accI, d * accD)) (0, 1) myData
This is due to the fact that (Monoid a, Monoid b) => Monoid (a, b)
. Now, what if you had custom data types and you wanted to fold over a tuple of these values applying a binary operation? You could simply write a newtype wrapper and make it an instance of Monoid
with that operation, construct your list of tuples, then just use mconcat
to fold across them. There are many other functions that work on Monoid
s as well, not just mconcat
, so there are certainly a myriad of applications.
You could also look at the First
and Last
newtype wrappers for Maybe a
, I can think of many uses for those. The Endo
wrapper is nice if you need to compose a lot of functions, the Any
and All
wrappers are good for working with booleans.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With