【问题标题】:What's the practical value of all those newtype wrappers in `Data.Monoid`?`Data.Monoid` 中所有这些新类型包装器的实用价值是什么?
【发布时间】:2014-03-31 14:14:44
【问题描述】:

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

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

对比更简洁的惯用变体

print $ sum [ 33, 2, 55 ]

但是有什么意义呢?所有这些 newtype 包装器有什么实用价值吗? Monoidnewtype wrapper 的用法比上面的例子更有说服力吗?

【问题讨论】:

  • 我不能说我曾经使用过它们。
  • sum = getSum . Data.Foldable.foldMap Sum

标签: haskell monoids


【解决方案1】:

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

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

由于它们是新类型,它们不会占用任何额外空间,并且应用 SumgetSum 是无操作的。

示例:Foldable 中的 Monoids

泛化 foldr 的方法不止一种(请参阅 this very good question 了解最通用的折叠,如果您喜欢下面的树示例但想查看最通用的树折叠,请参阅 this question)。

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

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

乍一看,这似乎令人沮丧,因为我们只能对每种数据类型使用一个二元运算 - 但我们应该使用 (+)0 来表示 Int 并求和但从不求积,还是反过来?也许我们应该将((+),0) 用于Int(*),1 用于Integer 并在我们需要其他操作时进行转换?这不会浪费很多宝贵的处理器周期吗?

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

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

滚动你自己的 Monoid

但是如果我们想找到最大元素怎么办?我们可以定义自己的幺半群。我不知道为什么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

结论

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

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

这就像将函数作为隐式参数而不是显式参数传递(因为无论如何这都是类型类所做的)。

【讨论】:

    【解决方案2】:

    在这样的情况下怎么样:

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


    您还可以查看Maybe aFirstLast 新类型包装器,我可以想到它们的许多用途。如果您需要编写大量函数,Endo 包装器非常适合,AnyAll 包装器非常适合处理布尔值。

    【讨论】:

    • 顺便说一句,第一个示例的输出与第二个不同
    • @danom 你确定吗?我只是将它复制/粘贴到 GHCi 中,我得到了相同的结果。我没有打开第一个示例的结果,但这很简单。
    • @danom 你可能会发现this 文章也很有帮助,它描述了使用Writer (Sum Integer) Integer 而不是State Integer Integer 来计算计算的步数等。它在类型签名中明确声明我们不能将该值相乘或对它做任何事情,除了向其添加另一个数字。
    【解决方案3】:

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

    您还需要newtype 才能使用像foldMap 这样具有Monoid 约束的函数。

    lens 包中的Control.Lens.Wrapped 中的alaalaf 组合子可以使使用这些新类型更加愉快。来自文档:

    >>> alaf Sum foldMap length ["hello","world"]
    10
    
    >>> ala Sum foldMap [1,2,3,4]
    10
    

    【讨论】:

      【解决方案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)
      

      如果您使用Monoid 新类型Firstf 专门用于Const 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 中。

      【讨论】:

      • 优秀的例子;它不是那么方便。当您在来自 Comonad 阅读器的非常值得阅读的 Abtracting with Applicatives 中转换/组合应用程序时,它被解释为一种向应用程序添加日志记录的方式。
      • 哦,这是一个更简单的例子。感谢您指出!
      • instance Monoid m =&gt; Applicative (Const m) 也是一个不是 monad 的应用程序的可爱示例 - 没有要绑定的 a。感谢您提出!
      • 这是我的两个首选示例之一!另一个是纯应用的 Either。
      【解决方案5】:

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

      reduce = foldl (<>) mempty
      

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

      【讨论】:

      • 难道reduce 不只是用mconcat 实现的foldl 而不是foldr
      猜你喜欢
      • 2020-03-13
      • 2016-09-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多