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

不想你离开。 提交于 2020-01-09 19:51:15

问题


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?


回答1:


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




回答2:


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




回答3:


Suppose you are working in the Writer monad and you want to store the sum of everything you tell. In that case you would need the newtype wrapper.

You would also need the newtype to use functions like foldMap that have a Monoid constraint.

The ala and alaf combinators from Control.Lens.Wrapped in the lens package can make working with these newtypes more pleasant. From the documentation:

>>> alaf Sum foldMap length ["hello","world"]
10

>>> ala Sum foldMap [1,2,3,4]
10



回答4:


Sometimes you just end up needing a particular Monoid to fill a type constraint. One place that shows up sometimes is that Const has an Applicative instance iff it stores a Monoid.

instance Monoid m => Applicative (Const m) where
  pure _ = Const mempty
  Const a <*> Const b = Const (a <> b)

That's obviously a bit bizarre, but sometimes it's what you need. The best example I know is in lens where you end up with types like

type Traversal s a = forall f . Applicative f => (a -> f a) -> (s -> f s)

If you specialize f to something like Const First using the Monoid newtype First

newtype First a = First { getFirst :: Maybe a }

-- Retains the first, leftmost 'Just'
instance Monoid (First a) where
  mempty = First Nothing
  mappend (First Nothing)  (First Nothing) = First Nothing
  mappend (First (Just x)) _               = First (Just x)

then we can interpret that type

(a -> Const (First a) a) -> (s -> Const (First a) s)

as scanning through s and picking up the first a inside of it.


So, while that's a really specific answer the broad response is that it's sometimes useful to be able to talk about a bunch of different default Monoid behaviors. Somebody had to write all the obvious Monoid behaviors, anyway, and they might as well be put in Data.Monoid.




回答5:


The basic idea, I think, is that you can have something like

reduce = foldl (<>) mempty

and it'll work for any list of those wrapped things.



来源:https://stackoverflow.com/questions/22080564/whats-the-practical-value-of-all-those-newtype-wrappers-in-data-monoid

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!