答案 0 :(得分:51)
通常,融合是指旨在消除中间数据结构的转换。您融合函数调用会导致浪费的内存分配更高效。这实际上是IMO Haskell最纯粹的应用之一。你几乎不需要做任何事情来获得它,它通过GHC编译器免费提供。
因为Haskell是纯粹的,我们得到这个名为referential transparency的东西,它(来自链接)意味着&#34;表达式总是在任何上下文中评估相同的结果&#34; 1 < / SUP>。这意味着我可以在不改变程序实际输出内容的情况下进行非常一般的程序级操作。例如,即使不知道x
,y
,z
和w
是什么,我也总是知道
((x ++ y) ++ z) ++ w
会评估与
相同的内容 x ++ (y ++ (z ++ w))
然而第二个实际上将涉及更少的内存分配(因为x ++ y
需要重新分配输出列表的整个前缀)。
事实上,我们可以做很多这样的优化,并且,因为Haskell是纯粹的,我们基本上只需移动整个表达式(替换x
,y
,{对于在上面的示例中评估为列表的实际列表或表达式,{1}}或z
不会更改任何内容。这成为一个非常机械的过程。
此外,事实证明,你可以为更高阶函数(Theorems for free!)提出许多等价。例如,
w
无论map f (map g xs) = map (f . g) xs
,f
和g
是什么(双方在语义上相等)。然而,虽然这个等式的两边产生相同的值输出,但左侧的效率总是更差:它最终为中间列表xs
分配空间,立即丢弃。我们想告诉编译器,只要遇到map g xs
之类的内容,就用map f (map g xs)
替换它。而且,对于GHC,这是通过rewrite rules:
map (f . g) xs
{-# RULES "map/map" forall f g xs. map f (map g xs) = map (f.g) xs #-}
,f
和g
可以与任何表达式匹配,而不仅仅是变量(因此xs
之类的内容会转换为map (+1) (map (*2) ([1,2] ++ [3,4]))
。 (There doesn't appear to be a good way to search for rewrite rules,所以我编译了一个list。This paper解释了GHC重写规则的动机和工作原理。
map ((+1) . (*2)) ([1,2] ++ [3,4])
?实际上,并不完全。上面的内容是short-cut fusion。这个名称意味着缺点:它没有太大的扩展性并且很难调试。您最终必须为相同的常用功能的所有安排编写大量的临时规则。然后,您希望重复应用重写规则可以很好地简化表达式。
事实证明,在某些情况下,我们可以通过组织我们的重写规则来做得更好,这样我们就可以构建一些中间正常形式,然后制定针对该中间形式的规则。通过这种方式,我们开始获得热销&#34;重写规则的路径。
这些系统中最先进的可能是stream fusion针对同源序列(基本上像列表这样的懒惰序列)。查看this thesis和this paper(实际上几乎是vector
包的实现方式)。例如,在map
中,您的代码首先会转换为涉及vector
和Stream
s的中间形式,并以该形式进行优化,然后转换回矢量。
Bundle
? Data.Text
使用流融合来最小化发生的内存分配数量(我认为这对于严格的变体尤为重要)。如果您查看source,则会看到功能&#34;受融合&#34;实际上大部分操纵Stream
s(它们是一般形式Data.Text
)并且有一堆unstream . (stuff manipulating stream) . stream
个编译指示用于转换RULES
s。最后,这些函数的任何组合都应该融合,以便只需要进行一次分配。
了解代码何时融合的唯一真正方法是充分了解所涉及的重写规则,并充分了解GHC的工作原理。也就是说,你有应该做的一件事:尽可能尝试使用非递归的高阶函数,因为这些函数可以(至少现在,但一般来说总是会更多)稠合的。
因为Haskell中的融合是通过重复应用重写规则而发生的,所以足以说服自己每个重写规则的正确性,以便知道整个&#34;融合&#34;程序与原始程序的功能相同。除了与程序终止有关的边缘情况。例如,有人可能会认为
Stream
但显然不是这样,因为 reverse (reverse xs) = xs
将不会终止head $ reverse (reverse [1..])
。 More information from the Haskell Wiki
1 只有在这些上下文中表达式保持相同的类型时,这才是真的。