Monoids for Maybe
The other day I had two lists of monoidal values that I wanted to combine in a certain way, and I realized it was an instance of this more general pattern:
> {-# LANGUAGE GeneralizedNewtypeDeriving #-}
> import Data.Monoid
> import Control.Applicative
>
> (<>) :: Monoid m => m -> m -> m
> (<>) = mappend -- I can't stand writing `mappend`
>
> newtype AM f m = AM { unAM :: f m }
> deriving (Functor, Applicative, Show)
>
> instance (Applicative f, Monoid m) => Monoid (AM f m) where
> mempty = pure mempty
> mappend f1 f2 = mappend <$> f1 <*> f2
It’s not too hard (although a bit fiddly) to show that AM f m satisfies the monoid laws, given that f and m satisfy the applicative functor and monoid laws respectively.
The basic idea here is that the mappend operation for AM f m is just the mappend operation for m, but applied "idiomatically" in the f context. For example, when f = [], this combines two lists of monoidal values by combining all possible pairs:
*Main> map getProduct . unAM $ (AM (map Product [1,2,3])
<> AM (map Product [1,10,100]))
[1,10,100,2,20,200,3,30,300]
In the #haskell IRC channel someone pointed out to me that Data.Monoid has an instance Monoid m => Monoid (e -> m) which is just a special case of this pattern:
*Main> :m +Data.Ord
*Main Data.Ord> map ((unAM $ AM (comparing length)
<> AM compare) "foo")
["ba", "bar", "barr"]
[GT,GT,LT]
*Main Data.Ord> map ((comparing length <> compare) "foo")
["ba", "bar", "barr"]
[GT,GT,LT]
It was also mentioned that the monoid instance for Maybe is also a special case of this pattern:
*Main> AM (Just (Sum 3)) <> AM Nothing
AM {unAM = Nothing}
*Main> Just (Sum 3) <> Nothing
Just (Sum {getSum = 3})
Wait, hold on, what?! It turns out that the default Monoid instance for Maybe is not an instance of this pattern after all! I previously thought there were three different ways of declaring a Monoid instance for Maybe; I now know that there are (at least) four.
-
The default instance defined in
Data.MonoidusesNothingas the identity element, soNothingrepresents "no information". It requires aMonoidconstraint on the type wrapped byMaybe, althoughMonoidis slightly too strong, since the type’s own identity element is effectively ignored. In fact, theData.Monoiddocumentation statesLift a semigroup into
Maybeforming aMonoidaccording to http://en.wikipedia.org/wiki/Monoid: "Any semigroup \(S\) may be turned into a monoid simply by adjoining an element \(e\) not in \(S\) and defining \(e*e = e\) and \(e*s = s = s*e\) for all \(s \in S\)." Since there is noSemigrouptype class providing justmappend, we useMonoidinstead.
Just (Sum {getSum = 3}) *Main> mconcat [Just (Sum 3), Nothing, Just (Sum 4), Nothing] Just (Sum {getSum = 7})<p>(Actually, there is (now) <a href="http://hackage.haskell.org/packages/archive/semigroups/0.3.4.1/doc/html/Data-Semigroup.html#t:Semigroup">a <code>Semigroup</code> type class</a>...)</p><pre><code>*Main> mconcat [Just (Sum 3), Nothing] -
The
Firstnewtype wrapper inData.Monoidjust takes the first non-Nothingoccurrence:*Main> mconcat . map First $ [Nothing, Just 3, Nothing, Just 4] First {getFirst = Just 3}This is actually the same as the
MonadPlusinstance forMaybe, wheremplusis used to choose the first non-failing computation:*Main Control.Monad> Nothing `mplus` Just 3 `mplus` Nothing `mplus` Just 4 Just 3 -
The
Lastnewtype wrapper is the dual ofFirst, taking the last non-Nothingoccurrence:*Main> mconcat . map Last $ [Nothing, Just 3, Nothing, Just 4] Last {getLast = Just 4} -
The
Monoidinstance following theApplicativestructure ofMaybe, however, is distinct from all of these. It combines values wrapped byJustaccording to their ownMonoidinstance, but if any occurrences ofNothingare encountered, the result is alsoNothing. That is, it corresponds to combining monoidal values in the presence of possible failure, that is, applyingmappendidiomatically within the applicative context.*Main> mconcat [AM (Just (Sum 3)), AM (Just (Sum 4))] AM {unAM = Just (Sum {getSum = 7})} *Main> mconcat [AM (Just (Sum 3)), AM (Just (Sum 4)), AM Nothing] AM {unAM = Nothing}As far as I know, this instance is nowhere to be found in the standard libraries. Perhaps a wrapper like
AMshould be added toControl.Applicative?