您所描述的本质上是一个幺半群。在 GHCI 中:
Prelude> :m + Data.Monoid
Prelude Data.Monoid> :info Monoid
class Monoid a where
mempty :: a
mappend :: a -> a -> a
mconcat :: [a] -> a
如你所见,一个幺半群具有三个相关的功能:
-
mempty 函数有点像幺半群的恒等函数。例如,Num 可以表现为一个幺半群,需要两个操作:求和和乘积。对于总和mempty 定义为0。对于产品mempty 定义为1。
mempty `mappend` a = a
a `mappend` mempty = a
mappend 函数类似于您的 union 函数。例如,Nums mappend 的总和定义为 (+),Nums 的乘积将 mappend 定义为 (*)。
-
mconcat 函数类似于折叠。然而,由于幺半群的特性,我们是从左侧折叠、从右侧折叠还是从任意位置折叠都无关紧要。这是因为mappend 应该是关联的:
(a `mappend` b) `mappend` c = a `mappend` (b `mappend` c)
但是请注意,Haskell 不执行幺半群定律。因此,如果您将一个类型设为 Monoid 类型类的实例,那么您有责任确保它满足幺半群定律。
在您的情况下,很难从其类型签名中理解 union 的行为方式:a -> a -> a。当然,您不能使类型变量成为类型类的实例。这是不允许的。你需要更具体。 union 究竟做了什么?
举个例子说明如何使一个类型成为 monoid 类型类的实例:
newtype Sum a = Sum { getSum :: a }
instance Num a => Monoid (Sum a) where
mempty = 0
mappend = (+)
就是这样。我们不需要定义mconcat 函数,因为它有一个依赖于mempty 和mappend 的默认实现。因此,当我们定义mempty 和mappend 时,我们会免费获得mconcat。
现在你可以使用Sum,如下:
getSum . mconcat $ map Sum [1..6]
这是正在发生的事情:
- 您将
Sum 构造函数映射到[1..6] 以生成[Sum 1, Sum 2, Sum 3, Sum 4, Sum 5, Sum 6]。
- 您将结果列表提供给
mconcat,后者将其折叠为Sum 21。
- 您使用
getSum 从Sum 21 中提取Num。
但是请注意mconcat 的默认实现是foldr mappend mempty(即它是一个正确的折叠)。对于大多数情况,默认实现就足够了。但是,在您的情况下,您可能希望覆盖默认实现:
foldParallel :: Monoid a => [a] -> a
foldParallel [] = mempty
foldParallel [a] = a
foldParallel xs = foldParallel left `mappend` foldParallel right
where size = length xs
index = (size + size `mod` 2) `div` 2
(left, right) = splitAt index xs
现在我们可以创建一个Monoid 的新实例,如下所示:
data Something a = Something { getSomething :: a }
instance Monoid (Something a) where
mempty = unionEmpty
mappend = union
mconcat = foldParallel
我们使用如下:
getSomething . mconcat $ map Something [1..6]
请注意,我将mempty 定义为unionEmpty。我不知道union 函数作用于什么类型的数据。因此我不知道应该将mempty 定义为什么。因此,我只是称它为unionEmpty。按照您认为合适的方式定义它。