为记录类型定义一个幺半群实例
Defining a monoid instance for a record type
假设我有一个像
这样的类型
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
当我的 Options
有更多的字段时,这将避免编写一堆 Nothing
的需要?
我建议只写 Nothing
s,甚至明确拼出所有记录字段,这样您就可以确保在添加具有不同 [=16 的新字段时不会错过任何一个案例=] 值,或重新排序字段:
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
, 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
在上下文中:
instance Monoid Options where
mempty = memptydefault
mappend = ...
如果您的记录类型的 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
) 取决于您想要的确切行为。
然后,假设您希望生成的幺半群与 Integer
之上的加法同步并使用 Sum
新类型,结果行为将是:
> 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})}
查看 generic-monoid package on hackage. Specifically, the Data.Monoid.Generic module. We can automatically derive the semigroup and monoid instances with the DerivingVia 扩展。这样,当您的记录很大并且记录中的每个字段都已经是幺半群时,您可以避免编写大量的 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
请记住,Haskell 要求如果你想定义一个幺半群,你需要一个半群。我们看到 typeclass of Monoid 具有 Semigroup
约束:
class Semigroup a => Monoid a where
...
不幸的是,在您的 Option
记录中并非所有字段都是幺半群。具体来说,Maybe Int
不满足 Semigroup
约束 out-of-the-box 因为 Haskell 不知道你要如何 mappend
两个 Int
,也许你会添加 (+)
它们或者你可能想乘以 (*)
它们等等。我们可以通过从 Data.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
是 Nothing
). First
的 mappend
总是选择第一个参数而不是第二个参数,它的 mempty
是 Nothing
。 Sum
的 mappend
只是添加 Integer
s 而其 mempty
为零 0
。 Maybe String
已经是一个幺半群,mappend
作为 String
连接,mempty
作为 Nothing
。一旦每个域都是幺半群,我们就可以通过 GenericSemigroup
和 GenericMonoid
.
推导出半群和幺半群
mempty :: Options
Options {
_optionOne = First { getFirst = Nothing },
_optionTwo = Sum { getSum = 0 },
_optionThree = Nothing
}
的确,mempty
符合我们的预期,我们不必为 Options
类型编写任何幺半群或半群实例。 Haskell 能够为我们导出它!
P.S。关于使用 Maybe a
作为幺半群的快速说明。它的 mempty
是 Nothing
,但它还要求 a
是一个半群。如果 mappend
的任何一个参数(或者因为我们正在谈论半群,它的 <>
)是 Nothing
,那么选择另一个参数。但是,如果两个参数都是 Just
,我们使用 a
的底层半群实例的 <>
.
instance Semigroup a => Semigroup (Maybe a) where
Nothing <> b = b
a <> Nothing = a
Just a <> Just b = Just (a <> b)
instance Semigroup a => Monoid (Maybe a) where
mempty = Nothing
假设我有一个像
这样的类型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
当我的 Options
有更多的字段时,这将避免编写一堆 Nothing
的需要?
我建议只写 Nothing
s,甚至明确拼出所有记录字段,这样您就可以确保在添加具有不同 [=16 的新字段时不会错过任何一个案例=] 值,或重新排序字段:
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
, 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
在上下文中:
instance Monoid Options where
mempty = memptydefault
mappend = ...
如果您的记录类型的 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
) 取决于您想要的确切行为。
然后,假设您希望生成的幺半群与 Integer
之上的加法同步并使用 Sum
新类型,结果行为将是:
> 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})}
查看 generic-monoid package on hackage. Specifically, the Data.Monoid.Generic module. We can automatically derive the semigroup and monoid instances with the DerivingVia 扩展。这样,当您的记录很大并且记录中的每个字段都已经是幺半群时,您可以避免编写大量的 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
请记住,Haskell 要求如果你想定义一个幺半群,你需要一个半群。我们看到 typeclass of Monoid 具有 Semigroup
约束:
class Semigroup a => Monoid a where
...
不幸的是,在您的 Option
记录中并非所有字段都是幺半群。具体来说,Maybe Int
不满足 Semigroup
约束 out-of-the-box 因为 Haskell 不知道你要如何 mappend
两个 Int
,也许你会添加 (+)
它们或者你可能想乘以 (*)
它们等等。我们可以通过从 Data.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
是 Nothing
). First
的 mappend
总是选择第一个参数而不是第二个参数,它的 mempty
是 Nothing
。 Sum
的 mappend
只是添加 Integer
s 而其 mempty
为零 0
。 Maybe String
已经是一个幺半群,mappend
作为 String
连接,mempty
作为 Nothing
。一旦每个域都是幺半群,我们就可以通过 GenericSemigroup
和 GenericMonoid
.
mempty :: Options
Options {
_optionOne = First { getFirst = Nothing },
_optionTwo = Sum { getSum = 0 },
_optionThree = Nothing
}
的确,mempty
符合我们的预期,我们不必为 Options
类型编写任何幺半群或半群实例。 Haskell 能够为我们导出它!
P.S。关于使用 Maybe a
作为幺半群的快速说明。它的 mempty
是 Nothing
,但它还要求 a
是一个半群。如果 mappend
的任何一个参数(或者因为我们正在谈论半群,它的 <>
)是 Nothing
,那么选择另一个参数。但是,如果两个参数都是 Just
,我们使用 a
的底层半群实例的 <>
.
instance Semigroup a => Semigroup (Maybe a) where
Nothing <> b = b
a <> Nothing = a
Just a <> Just b = Just (a <> b)
instance Semigroup a => Monoid (Maybe a) where
mempty = Nothing