Chapter 12 MONOIDS
This chapter features another useful and fun type class: Monoid. This type class is for types whose values can be combined together with a binary operation.
Table of Contents
1 Wrapping an Existing Type into a New Type
The newtype keyword in Haskell is made exactly for cases when we want to just take one type and wrap it in something to present it as another type. In the actual libraries, ZipList a is defined like this:
newtype ZipList a = ZipList { getZipList :: [a] }
1.1 Using newtype to Make Type Class Instances
we can newtype our tuple in such a way that the second type parameter represents the type of the first component in the tuple
newtype Paire b a = Paire { getPair :: (a,b) }
And now we can make it an instance of Functor so that the function is mapped over the first component.
instance Functor (Pair c) where fmap f (Pair (x, y)) = Pair (f x, y)
ghci>getPair $ fmap (*100) (Pair (2,3)) (200,3) ghci>getPair $ fmap reverse (Pair ("asdf",2)) ("fdsa",2)
1.2 On newtype Laziness
The undefined value in Haskell represents an erroneous computation. If we try to evaluate it (that is, force Haskell to actually compute it) by printing it to the terminal, Haskell will throw a hissy fit (technically referred to as an exception):
ghci>undefined *** Exception: Prelude.undefined
Here's another example.
ghci>head [32,1,undefined,32,undefined] 32
This time,no exception,because Haskell is lazy,when we use head, we don't need to evaluate undefined which is not the "head".
Now consider the following type and a function:
data CoolBool = CoolBool { getCoolBool :: Bool } helloMe :: CoolBool -> String helloMe (CoolBool _) = "hello"
Let's test it.
ghci>helloMe (CoolBool True ) "hello" ghci>helloMe undefined "*** Exception: Prelude.undefined
Why did this exception happen? Types defined with the data keyword can have multiple value constructors (even though CoolBool has only one). So in order to see if the value given to our function conforms to the (CoolBool _) pattern, Haskell must evaluate the value just enough to see which value constructor was used when we made the value. And when we try to evaluate an undefined value, even a little, an exception is thrown.
Instead of using data keyword, we use newtype.
newtype CoolBool = CoolBool { getCoolBool :: Bool } helloMe :: CoolBool -> String helloMe (CoolBool _) = "hello"
Let's test it again.
ghci>helloMe (CoolBool True ) "hello" ghci>helloMe undefined "hello"
It worked! Hmmm, why is that? Well, as you’ve learned, when you use newtype, Haskell can internally represent the values of the new type in the same way as the original values. because Haskell knows that types made with the newtype keyword can have only one constructor, it doesn’t need to evaluate the value passed to the function to make sure that the value conforms to the (CoolBool _) pattern, because newtype types can have only one possible value constructor and one field!
1.3 type vs. newtype vs. data
At this point, you may be a bit confused about the differences between type, data, and newtype, so let’s review their uses. The type keyword is for making type synonyms. We use type synonyms when we want to make our type signatures more descriptive.
type IntList = [Int]
The newtype keyword is for taking existing types and wrapping them in new types, mostly so it’s easier to make them instances of certain type classes. Suppose we make the following newtype:
newtype CharList = CharList { getCharList :: [Char] }
We can’t use ++ to put together a CharList and a list of type [Char]. We can’t even use ++ to put together two CharList lists, because ++ works only on lists, and the CharList type isn’t a list, even though it could be said that CharList contains a list. We can, however, convert two CharLists to lists, ++ them, and then convert that back to a CharList. In practice, you can think of newtype declarations as data declarations that can have only one constructor and one field. If you catch yourself writ- ing such a data declaration, consider using newtype.
2 About Those Monoids
2.1 The Monoid Type Class
A monoid is made up of an associative binary function and a value that acts as an identity with respect to that function.For example,1 is the identity with respect to *, and [] is the identity with respect to ++. There are a lot of other monoids to be found in the world of Haskell, which is why the Monoid type class exists. Let’s see how the type class is defined:
class Monoid m where mempty :: m mappend :: m -> m -> m mconcat :: [m] -> m mconcat = foldr mappend mempty
First, we see that only concrete types can be made instances of Monoid, because the m in the type class definition doesn’t take any type parameters.
First,*mempty* represents the identity value for a particular monoid.
Next up, we have mappend, which, as you’ve probably guessed, is the bi- nary function. It takes two values of the same type and returns another value of that same type.
The last function in this type class definition is mconcat. It takes a list of monoid values and reduces them to a single value by using mappend between the list’s elements.
2.2 The Monoid Laws
- mempty `mappend` x = x
- x `mappend` mempty = x
- (x `mappend` y) `mappend` z = x `mappend` (y `mappend` z)
3 Meet Some Monoids
3.1 Lists Are Monoids
instance Monoid [a] where mempty = [] mappend = (++)
Notice that we wrote instance Monoid [a] and not instance Monoid [], because Monoid requires a concrete type for an instance.
ghci>[1,2,3] `mappend` mempty [1,2,3] ghci>mempty `mappend` [1,2,3] [1,2,3] ghci>"hi" `mappend` ",John" "hi,John" ghci>("1" `mappend ` "2") `mappend ` "3" "123" ghci>"1" `mappend ` ("2" `mappend ` "3") "123"
3.2 Product and Sum
We already examined one way for numbers to be considered monoids: Just let the binary function be * and the identity value be 1. Another way for numbers to be monoids is to have the binary function be + and the identity value be 0. With two equally valid ways for numbers to be monoids, which way do we choose? Well, we don’t have to pick. Remember that when there are several ways for some type to be an instance of the same type class, we can wrap that type in a newtype and then make the new type an instance of the type class in a different way. The Data.Monoid module exports two types for this: Product and Sum. Product is defined like this:
newtype Product a = Product { getProduct :: a} deriving (Eq, Ord, Read, Show, Bounded)
It's instance for Monoid goes something like this:
instance Num a => Monoid (Product a) where mempty = Product 1 Product x `mappend` Product y = Product (x*y)
ghci> Product 3 `mappend` Product 5 Product {getProduct = 15} ghci>getProduct $ Product 3 `mappend` Product 5 15 ghci>getProduct . mconcat . map Product $ [3,4,5] 60
3.3 Any and All
Another type that can act like a monoid in two distinct but equally valid ways is Bool. The first way is to have the function ||, which represents a logical OR, act as the binary function along with False as the identity value. The Any newtype constructor is an instance of Monoid in this fashion. It’s defined like this.
newtype Any = Any { getAny :: Bool } deriving (Eq, Ord, Read, Show, Bounded)
instance Monoid Any where mempty = Any False Any x `mappend` Any y = Any (x || y)
ghci>getAny $ Any True `mappend` Any False True ghci>getAny $ Any False `mappend` Any False False ghci>getAny $ mempty `mappend` mempty False ghci>getAny $ mempty `mappend` Any True True ghci>getAny $ mempty `mappend` Any False False ghci>getAny . mconcat . map Any $ [False, False, False, True] True
3.4 The Ordering Monoid
Remember the Ordering type? It’s used as the result when comparing things, and it can have three values: LT, EQ, and GT,
ghci>1 `compare` 2 LT ghci>2 `compare` 2 EQ ghci>3 `compare` 2 GT
Ordering is instance of Monoid too.
instance Monoid Ordering where mempty = EQ LT `mappend` _ = LT EQ `mappend` y = y GT `mappend` _ = GT
ghci>LT `mappend` GT LT ghci>GT `mappend` LT GT ghci>mempty `mappend` LT LT
Okay, so how is this monoid useful? Here’s one way to write this:
lengthCompare :: String -> String -> Ordering lengthCompare x y = let a = length x `compare` length y b = x `compare` y in if a == EQ then b else a
But by employing our understanding of how Ordering is a monoid, we can rewrite this function in a much simpler manner:
lengthCompare :: String -> String -> Ordering lengthCompare x y = (length x `compare` length y) `mappend` (x `compare` y)
3.5 Maybe the Monoid
Let’s take a look at the various ways that Maybe a can be made an instance of Monoid and how those instances are useful. Here’s the instance declaration
instance Monoid a => Monoid (Maybe a) where mempty = Nothing Nothing `mappend` m = m m `mappend` Nothing = m Just m1 `mappend` Just m2 = Just (m1 `mappend` m2)
f we mappend two Just values, the contents of the Justs are mappended and then wrapped back in a Just. We can do this because the class constraint ensures that the type of what’s inside the Just is an in- stance of Monoid.
ghci>Nothing `mappend` Just "andy" Just "andy" ghci>Just LT `mappend` Nothing Just LT ghci>Just (Sum 3) `mappend` Just (Sum 4) Just (Sum {getSum = 7})
4 Folding with Monoids
One of the more interesting ways to put monoids to work is to have them help us define folds over various data structures. So far, we’ve done folds over lists, but lists aren’t the only data structure that can be folded over. We can define folds over almost any data structure. Trees especially lend themselves well to folding.
Because there are so many data structures that work nicely with folds, the Foldable type class was introduced. Much like Functor is for things that can be mapped over, Foldable is for things that can be folded up!
It can be found in Data.Foldable, and because it exports functions whose names clash with the ones from the Prelude, it’s best imported qualified.
import qualified Data.Foldable as F
Let’s compare the types of Foldable’s foldr and foldr from Prelude to see how they differ
ghci>:t foldr foldr :: (a -> b -> b) -> b -> [a] -> b ghci>:t F.foldr F.foldr :: F.Foldable t => (a -> b -> b) -> b -> t a -> b
ghci>foldr (*) 1 [1,2,3] 6 ghci>F.foldr (*) 1 [1,2,3] 6
Another data structures that support folds is the Maybe we all know and love!
ghci>F.foldr (+) 2 (Just 9) 11 ghci>F.foldr (||) False (Just True) True ghci>foldr (+) 2 (Just 9) error...
Remember the tree data structure from Chapter 7?
data Tree a = EmptyTree | Node a (Tree a) (Tree a) deriving (Show)
One way to make a type constructor an instance of Foldable is to just di- rectly implement foldr for it. But another, often much easier way, is to im- plement the foldMap function, which is also a part of the Foldable type class. The foldMap function has the following type:
foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m
This is how we make Tree an instance of Foldable:
instance F.Foldable Tree where foldMap f EmptyTree = mempty foldMap f (Node x l r) = F.foldMap f l `mappend` f x `mappend` F.foldMap f r
Date: 2013-01-26 19:49:12 CST