假设我有类似
的类型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
的内容吗?
答案 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}},Last
或Sum
:
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
包裹到Sum
或Product
(或可能包含其他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时编写大量的mappend
和mempty
函数。该文档提供了以下示例:
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}}总是选择第一个参数而不是第二个参数,并且其First
是mappend
。 mempty
的{{1}}仅添加了Nothing
,其Sum
为零mappend
。 Integer
已经是具有mempty
作为0
串联和Maybe String
作为mappend
的单面体。一旦每个字段都是一个Monoid,我们就可以通过String
和mempty
得出半群和monoid。
Nothing
实际上,GenericSemigroup
符合我们的期望,我们不必为GenericMonoid
类型编写任何monoid或半组实例。 Haskell能够为我们导出它!
P.S。关于将mempty :: Options
Options {
_optionOne = First { getFirst = Nothing },
_optionTwo = Sum { getSum = 0 },
_optionThree = Nothing
}
用作monoid的快速说明。它的mempty
是Options
,但它也需要Maybe a
是一个半群。如果mempty
的一个自变量(或者因为我们在谈论半群的Nothing
)是a
,则选择另一个自变量。但是,如果两个参数均为mappend
,我们将使用<>
的基础半组实例的Nothing
。
Just