17

在查看 时Data.Monoid,我看到有各种newtype包装器,例如AllSumProduct,它们编码各种类型的幺半群。但是,当尝试使用这些包装器时,我不禁想知道使用它们的非Data.Monoid对应物有什么好处。比如比较比较繁琐的求和

print $ getSum $ mconcat [ Sum 33, Sum 2, Sum 55 ]

与更简洁的惯用变体相比

print $ sum [ 33, 2, 55 ]

但有什么意义呢?newtype拥有所有这些包装器有什么实用价值吗?是否有Monoid newtype比上述更令人信服的包装器使用示例?

4

5 回答 5

33

Monoid newtypes:一个零空间无操作告诉编译器该做什么

Monoids 非常适合将现有数据类型包装在新类型中,以告诉编译器您要执行什么操作。

由于它们是新类型,因此它们不会占用任何额外的空间并且应用Sum或者getSum是空操作。

示例:Foldable 中的 Monoids

概括 foldr 的方法不止一种(对于最一般的折叠,请参阅这个非常好的问题,如果您喜欢下面的树示例但想查看最一般的树折叠,请参阅这个问题)

一种有用的方法(不是最通用的方法,但绝对有用)是说某些东西是可折叠的,如果您可以将其元素与二进制操作和开始/身份元素组合成一个。这就是类型类的重点Foldable

无需显式传入二进制操作和起始元素,Foldable只需询问元素数据类型是 Monoid 的实例。

乍一看,这似乎令人沮丧,因为我们只能对每种数据类型使用一个二元运算——但我们应该使用(+)and 0forInt和求和而不是乘积,还是反过来呢?当我们想要其他操作时,也许我们应该使用((+),0)forInt(*),1forInteger和 convert ?这不会浪费很多宝贵的处理器周期吗?

Monoids 救援

我们需要做的就是Sum如果我们想要添加标记,Product如果我们想要乘以标记,或者如果我们想要做一些不同的事情,甚至可以使用手动新类型标记。

让我们折一些树!我们需要

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

如果您想映射和折叠您自己的 ADT 而无需自己编写繁琐的实例,则 和 扩展 ( )非常DeriveFunctor有用DeriveFoldable{-# LANGUAGE DeriveFunctor, DeriveFoldable #-}

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

示例用法

(++)使用and字符串已经是一个幺半群[],所以我们可以fold使用它们,但数字不是,所以我们将使用 标记它们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

滚动你自己的 Monoid

但是如果我们想找到最大元素呢?我们可以定义自己的幺半群。我不确定为什么Max(and Min) 不在。也许是因为没有人喜欢考虑Int被限制,或者他们只是不喜欢基于实现细节的标识元素。无论如何,这里是:

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

结论

我们可以使用 newtype Monoid 包装器来告诉编译器以哪种方式将事物成对组合。

标签什么都不做,但显示要使用的组合功能。

这就像将函数作为隐式参数而不是显式参数传递(因为这就是类型类所做的事情)。

于 2014-02-27T23:18:13.077 回答
9

在这样的情况下怎么样:

myData :: [(Sum Integer, Product Double)]
myData = zip (map Sum [1..100]) (map Product [0.01,0.02..])

main = print $ mconcat myData

或者没有 newtype 包装器和Monoid实例:

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

这是由于(Monoid a, Monoid b) => Monoid (a, b). 现在,如果您有自定义数据类型,并且想要通过二进制操作折叠这些值的元组怎么办?您可以简单地编写一个新类型包装器并使用该操作使其成为一个实例Monoid,构建您的元组列表,然后只需使用mconcat它们来折叠它们。还有许多其他功能也可以在Monoids 上运行,而不仅仅是mconcat,因此肯定有无数的应用程序。


您还可以查看FirstLastnewtype 包装器Maybe a,我可以想到这些包装器的许多用途。如果Endo您需要组合很多函数,则包装器非常好,Any并且All包装器非常适合处理布尔值。

于 2014-02-27T21:40:44.853 回答
6

假设您在Writermonad 中工作,并且您想要存储所有内容的总和tell。在这种情况下,您将需要newtype包装器。

您还需要newtype使用foldMap具有Monoid约束的功能。

包中的alaalaf组合器可以使使用这些新类型更加愉快。从文档中:Control.Lens.Wrappedlens

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

>>> ala Sum foldMap [1,2,3,4]
10
于 2014-02-27T21:32:31.743 回答
4

有时你最终需要一个特定Monoid的来填充类型约束。有时会出现Const的一个地方是,Applicative如果它存储了一个Monoid.

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

这显然有点奇怪,但有时这正是你所需要的。我知道的最好的例子是lens你最终得到的类型是

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

如果您专注于使用newtype之f类的东西Const FirstMonoidFirst

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)

然后我们可以解释该类型

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

就像扫描s并拾取它的第一个a内部一样。


因此,虽然这是一个非常具体的答案,但广泛的回应是,有时能够谈论一堆不同的默认Monoid行为很有用。无论如何,必须有人写下所有明显的Monoid行为,它们还不如放进去Data.Monoid

于 2014-02-28T01:54:41.387 回答
2

我认为,基本的想法是你可以有类似的东西

reduce = foldl (<>) mempty

它适用于任何包装物品的列表。

于 2014-02-27T21:19:02.287 回答