为记录类型定义monoid实例

时间:2018-03-27 15:55:32

标签: haskell typeclass monoids

假设我有类似

的类型
data Options = Options
  { _optionOne :: Maybe Integer
  , _optionTwo :: Maybe Integer
  , _optionThree :: Maybe String
  } deriving Show

还有更多领域。我想为此类型定义Monoid个实例,mempty值为Options,其中包含所有字段Nothing。是否有比

更简洁的方式来写这个
instance Monoid Options where
  mempty = Options Nothing Nothing Nothing
  mappend = undefined

当我的Nothing有更多字段时,这会避免写出一堆Options的内容吗?

3 个答案:

答案 0 :(得分:4)

我建议您只编写Nothing,或者甚至明确拼写出所有记录字段,这样您就可以确保在添加不同mempty的新字段时不会错过任何一个案例值或重新排序字段:

mempty = Options
  { _optionOne = Nothing
  , _optionTwo = Nothing
  , _optionThree = Nothing
  }

我之前没有尝试过,但似乎您可以使用generic-deriving包来实现此目的,只要您的记录的所有字段都是Monoid s。您将添加以下语言编译指示和导入:

{-# LANGUAGE DeriveGeneric #-}
import GHC.Generics (Generic)
import Generics.Deriving.Monoid

deriving (Generic)添加到您的数据类型,并将Monoid类型中的所有非Data.Monoid字段包含在您想要的组合行为中,例如First,{ {1}},LastSum

Product

示例:

  • data Options = Options { _optionOne :: Last Integer , _optionTwo :: Last Integer , _optionThree :: Maybe String } deriving (Generic, Show) = Last (Just 2) <> Last (Just 3)
  • Last {getLast = Just 3} = First (Just 2) <> First (Just 3)
  • First {getFirst = Just 2} = Sum 2 <> Sum 3
  • Sum {getSum = 5} = Product 2 <> Product 3

然后使用Product {getProduct = 6}中的以下函数创建默认实例:

Generics.Deriving.Monoid

在上下文中:

memptydefault :: (Generic a, Monoid' (Rep a)) => a
mappenddefault :: (Generic a, Monoid' (Rep a)) => a -> a -> a

答案 1 :(得分:2)

如果您的记录类型的Monoid实例自然地来自记录字段的Monoid个实例,那么您可以使用Generics.Deriving.Monoid。代码可能如下所示:

{-# LANGUAGE DeriveGeneric #-}

import GHC.Generics
import Generics.Deriving.Monoid

data Options = { .. your options .. }
             deriving (Show, Generic)

instance Monoid Options where
  mempty = memptydefault
  mappend = mappenddefault

请注意,记录字段也必须为Monoid,因此您必须将Integer包裹到SumProduct(或可能包含其他newtype 1}})取决于你想要的确切行为。

然后,假设您希望生成的monoid与Integer之上的添加同步并使用Sum newtype,结果行为将是:

> mempty :: Options
Options {_optionOne = Nothing, _optionTwo = Nothing, _optionThree = Nothing}
> Options (Just $ Sum 1) (Just $ Sum 2) (Just $ Sum 3) <> Options (Just $ Sum 1) (Just $ Sum 2) Nothing
Options {_optionOne = Just (Sum {getSum = 2}), _optionTwo = Just (Sum {getSum = 4}), _optionThree = Just (Sum {getSum = 3})}

答案 2 :(得分:1)

查看有关黑客的generic-monoid软件包。具体来说,是Data.Monoid.Generic模块。我们可以自动导出具有DerivingVia扩展名的半组和monoid实例。这样,您可以避免在记录很大并且记录中的每个字段都已经是一个monoid时编写大量的mappendmempty函数。该文档提供了以下示例:

data X = X [Int] String
  deriving (Generic, Show, Eq)
  deriving Semigroup via GenericSemigroup X
  deriving Monoid    via GenericMonoid X

之所以有效,是因为[Int]是一个单面体,而String是一个单面体。在这两个字段中,mappend是串联的,mempty是空列表[]和空字符串""。因此,我们可以使X为一个等宽线。

X [] "" == (mempty :: X)
True

请记住,如果要定义Monoid,Haskell要求您需要一个半组。我们看到typeclass of Monoid具有Semigroup约束:

class Semigroup a => Monoid a where
 ...

不幸的是,您的Option记录中并非所有字段都是monoid。具体来说,Maybe Int不能立即满足Semigroup约束,因为Haskell不知道您想mappend两个Int,也许您会添加(+),或者您想将(*)乘以此类推。我们可以轻松地解决此问题,方法是从Data.Monoid借用常见的monoid(或编写我们自己的),并将其中的所有字段Option个半身像。

{-# DeriveGeneric #-}
{-# DerivingVia   #-}

import GHC.Generics
import Data.Monoid
import Data.Monoid.Generic

data Options = Options
  { _optionOne   :: First Integer
  , _optionTwo   :: Sum   Integer
  , _optionThree :: Maybe String
  } 
  deriving (Generic, Show, Eq)
  deriving Semigroup via GenericSemigroup Options
  deriving Monoid    via GenericMonoid Options

您未在问题中定义mappend函数,因此我只是随机选择了一些类半身像来显示多样性(您可能会发现Maybe wrappers很有趣,因为它们的mempty是{{1} })。 Nothing的{​​{1}}总是选择第一个参数而不是第二个参数,并且其Firstmappendmempty的{​​{1}}仅添加了Nothing,其Sum为零mappendInteger已经是具有mempty作为0串联和Maybe String作为mappend的单面体。一旦每个字段都是一个Monoid,我们就可以通过Stringmempty得出半群和monoid。

Nothing

实际上,GenericSemigroup符合我们的期望,我们不必为GenericMonoid类型编写任何monoid或半组实例。 Haskell能够为我们导出它!

P.S。关于将mempty :: Options Options { _optionOne = First { getFirst = Nothing }, _optionTwo = Sum { getSum = 0 }, _optionThree = Nothing } 用作monoid的快速说明。它的memptyOptions,但它也需要Maybe a是一个半群。如果mempty的一个自变量(或者因为我们在谈论半群的Nothing)是a,则选择另一个自变量。但是,如果两个参数均为mappend,我们将使用<>的基础半组实例的Nothing

Just