What's the practical value of all those newtype wrappers in `Data.Monoid`?

前端 未结 5 452
误落风尘
误落风尘 2020-12-15 21:13

When looking at Data.Monoid, I see there are various newtype wrappers, such as All, Sum, or Product, which e

5条回答
  •  失恋的感觉
    2020-12-15 21:20

    Monoid newtypes: A zero space no-op to tell the compiler what to do

    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.

    Example: Monoids in Foldable

    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?

    Monoids to the rescue

    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 " []]]
    

    Example usage

    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
    

    Roll your own Monoid

    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
    

    Conclusion

    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).

提交回复
热议问题