在查看Data.Monoid
时,我发现有各种newtype
包装,例如All
,Sum
或Product
,它们编码各种类型的类群。但是,在尝试使用这些包装器时,我不禁想知道使用非Data.Monoid
对应物的好处是什么。例如,比较相当繁琐的总和
print $ getSum $ mconcat [ Sum 33, Sum 2, Sum 55 ]
VS。更简洁的惯用变体
print $ sum [ 33, 2, 55 ]
但重点是什么?所有那些newtype
包装器是否有任何实用价值?是否有比{2}更有说服力的Monoid
newtype
包装使用示例?
答案 0 :(得分:29)
Monoids非常适合将现有数据类型包装在新类型中,以告诉编译器您要执行的操作。
由于它们是新类型,因此它们不占用任何额外空间,而应用Sum
或getSum
则是无操作。
有多种方法可以概括折叠(对于最常见的折叠,请参阅this very good question;如果您喜欢下面的树示例,但希望看到树的最常见折叠,请this question。
一种有用的方法(不是最通用的方法,但绝对有用)是说如果可以将其元素与二元运算和开始/标识元素组合成一个可折叠的东西。这就是Foldable
类型类的重点。
Foldable
只是要求元素数据类型是Monoid的一个实例,而不是显式传入二进制操作和启动元素。
(+)
使用0
和Int
来获取总和而不是产品,或者反过来呢?也许我们应该((+),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
DeriveFunctor
和DeriveFoldable
扩展程序({-# 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
但是,如果我们想找到最大元素呢?我们可以定义自己的幺半群。我不确定为什么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
包装器很好,Endo
和Any
包装器适合使用布尔值。
答案 2 :(得分:5)
假设您在Writer
monad中工作,并且希望存储tell
所有内容的总和。在这种情况下,您需要newtype
包装器。
您还需要newtype
使用具有foldMap
约束的Monoid
等函数。
ala
包中来自Control.Lens.Wrapped
的alaf
和lens
组合器可以使这些新类型更加愉快。来自文档:
>>> 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
它适用于任何包装物品的清单。