我有一个类型如下的函数:
union :: a -> a -> a
a
具有可加性属性。因此,我们可以将union
视为(+)
说,我们有[a]
,并希望执行并行"folding"
,对于非并行折叠,我们只能这样做:
foldl1' union [a]
但如何并行执行?
我可以在Num
值和(+)
函数上演示问题。
例如,我们有一个列表[1,2,3,4,5,6]
和(+)
同时我们应该拆分
[1,2,3] (+) [4,5,6]
[1,2] (+) [3] (+) [4,5] (+) [6]
([1] (+) [2]) (+) ([3] (+) [4]) (+) ([5] (+) [6])
然后我们要并行执行每个(+)
操作,并结合回答
[3] (+) [7] (+) [11] = 21
请注意,由于a
可加性,我们会拆分列表或以任何顺序执行操作。
有没有办法使用任何标准库?
答案 0 :(得分:12)
您需要将union
推广到任何关联二元运算符⊕,使得(a⊕b)⊕c==a⊕(b⊕c)。如果同时你甚至有一个相对于neutral中性的单位元素,你就有一个幺半群。
关联性的一个重要方面是你可以在一个列表中任意分组连续元素的块,并且它们可以按任何顺序排列,因为⊕(b⊕(c⊕d))==(a⊕b)⊕( c⊕d) - 每个支架可以并行计算;那么你需要“减少”所有括号的“总和”,并且你已经对map-reduce进行了排序。
为了让这种并行化有意义,你需要使分块操作比⊕更快 - 否则,顺序执行is比分块更好。一个这样的情况是当你有一个随机访问“列表” - 比如一个数组。 Data.Array.Repa有很多平行折叠功能。
如果您正在考虑自己实施一个实践,那么您需要选择一个好的复杂功能,以便获得好处。
例如:
import Control.Parallel
import Data.List
pfold :: (Num a, Enum a) => (a -> a -> a) -> [a] -> a
pfold _ [x] = x
pfold mappend xs = (ys `par` zs) `pseq` (ys `mappend` zs) where
len = length xs
(ys', zs') = splitAt (len `div` 2) xs
ys = pfold mappend ys'
zs = pfold mappend zs'
main = print $ pfold (+) [ foldl' (*) 1 [1..x] | x <- [1..5000] ]
-- need a more complicated computation than (+) of numbers
-- so we produce a list of products of many numbers
在这里,我故意使用一个仅在本地称为mappend
的关联操作,以表明它可以用于比幺半群更弱的概念 - 只有关联性对并行性很重要;因为并行性只对非空列表有意义,所以不需要mempty
。
ghc -O2 -threaded a.hs
a +RTS -N1 -s
总运行时间为8.78秒,而
a +RTS -N2 -s
在我的双核笔记本电脑上总运行时间为5.89秒。显然,在这台机器上尝试超过-N2没有意义。
答案 1 :(得分:4)
你所描述的基本上是一个幺半群。在GHCI:
Prelude> :m + Data.Monoid
Prelude Data.Monoid> :info Monoid
class Monoid a where
mempty :: a
mappend :: a -> a -> a
mconcat :: [a] -> a
正如你所看到的,monoid有三个相关的函数:
mempty
函数有点像幺半群的身份函数。例如,Num
可以表现为monoid apropos两个操作:sum和product。总和mempty
定义为0
。对于产品mempty
,定义为1
。
mempty `mappend` a = a
a `mappend` mempty = a
mappend
功能类似于您的union
功能。例如,Num
s mappend
的总和定义为(+)
,而Num
s 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
从Num
中提取Sum 21
。但请注意,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
。根据您的需要定义它。