`Data.Monoid`中所有newtype包装器的实用价值是什么?

时间:2014-02-27 21:15:22

标签: haskell monoids

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

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

VS。更简洁的惯用变体

print $ sum [ 33, 2, 55 ]

但重点是什么?所有那些newtype包装器是否有任何实用价值?是否有比{2}更有说服力的Monoid newtype包装使用示例?

5 个答案:

答案 0 :(得分:29)

Monoid newtypes:零空间无操作,告诉编译器要做什么

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

由于它们是新类型,因此它们不占用任何额外空间,而应用SumgetSum则是无操作。

示例:可折叠的幺半群

有多种方法可以概括折叠(对于最常见的折叠,请参阅this very good question;如果您喜欢下面的树示例,但希望看到树的最常见折叠,请this question

一种有用的方法(不是最通用的方法,但绝对有用)是说如果可以将其元素与二元运算和开始/标识元素组合成一个可折叠的东西。这就是Foldable类型类的重点。

Foldable只是要求元素数据类型是Monoid的一个实例,而不是显式传入二进制操作和启动元素。

乍一看,这似乎很令人沮丧,因为我们每个数据类型只能使用一个二进制操作 - 但是我们应该(+)使用0Int来获取总和而不是产品,或者反过来呢?也许我们应该((+),0)使用Int(*),1使用Integer并在我们想要其他操作时进行转换?这不会浪费很多宝贵的处理器周期吗?

拯救蠢货

我们需要做的就是使用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

DeriveFunctorDeriveFoldable扩展程序({-# LANGUAGE DeriveFunctor, DeriveFoldable #-})非常棒,如果您想要自己映射并折叠自己的ADT而无需自己编写繁琐的实例。

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

使用示例

字符串已使用(++)[]作为幺半群,因此我们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 标记它们

Max

滚动自己的Monoid

但是,如果我们想找到最大元素呢?我们可以定义自己的幺半群。我不确定为什么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
{{1}}

结论

我们可以使用newtype Monoid包装器告诉编译器成对组合事物的方法。

标签不执行任何操作,但显示要使用的组合功能。

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

答案 1 :(得分: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)。现在,如果您有自定义数据类型并且想要应用二进制操作来折叠这些值的元组,那该怎么办?您可以简单地编写一个newtype包装器,并使用该操作使其成为Monoid的实例,构建您的元组列表,然后使用mconcat折叠它们。还有许多其他功能也适用于Monoid,而不仅仅是mconcat,所以肯定有无数的应用程序。


您还可以查看First的{​​{1}}和Last新类型包装器,我可以想到它们的许多用途。如果你需要编写很多函数,Maybe a包装器很好,EndoAny包装器适合使用布尔值。

答案 2 :(得分:5)

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

您还需要newtype使用具有foldMap约束的Monoid等函数。

ala包中来自Control.Lens.Wrappedalaflens组合器可以使这些新类型更加愉快。来自文档:

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

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

答案 3 :(得分: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) ,你最终会得到像

这样的类型
f

如果您使用Const First newtype Monoid

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) 之类的内容
(a -> Const (First a) a) -> (s -> Const (First a) s)

然后我们可以解释那种类型

s

扫描a并获取其中的第一个Monoid


所以,虽然这是一个非常具体的答案,但广泛的回答是能够谈论一堆不同的默认Monoid行为有时很有用。无论如何,有人必须编写所有明显的Data.Monoid行为,并且它们也可能被放入{{1}}。

答案 4 :(得分:1)

我认为,基本的想法是,你可以拥有像

这样的东西
reduce = foldl (<>) mempty

它适用于任何包装物品的清单。