我目前正在学习Haskell(通过交易成为程序员,但这是我第一次使用函数式语言)。
我想编写一个扫描列表并返回该列表的最小和最大元素的函数。 Prelude函数minimum
和maximum
的功能排序,但两者同时进行。我提出了以下代码:
import Data.List
-- Declaration of rand
minMax :: [Int] -> Maybe (Int, Int)
minMax [] = Nothing
minMax (x:xs) = Just (foldl' f (x, x) xs)
where
f (a, b) c = (if c < a then c else a, if c > b then c else b)
rand
是一个生成无限数字列表的函数。
问题是当我附加以下main
函数时:
main = print $ minMax $ take 1000000 $ rand 7666532
使用性能分析编译并运行所有这些,它显示它使用超过200 MB的内存,所以它绝对不是一个恒定空间函数(我喜欢它)。
问题是为什么以及应该修改什么来修复它。据我所知,foldl'
从左侧折叠列表(它生成的方式相同)并且不是懒惰的,所以我不明白为什么内存使用率如此之高。我很确定minMax
函数不正确,只需打印上述列表,使用
main = print $ take 1000000 $ rand 7666532
给了我1MB的使用量,这是我理解和期待的。
答案 0 :(得分:26)
请注意foldl'
强制累加器为弱头部正常形式。由于累加器是一个元组,它不强制评估元组的两个元素。
如果明确强制使用这两个元素,则会得到一个常量空间函数:
f (a, b) c = a `seq` b `seq` (if c < a then c else a, if c > b then c else b)
在原始程序中,您正在构建一个类型的元组:(<thunk>, <thunk>)
和每次应用f
时,您只需构建一个具有越来越大的thunk的元组。最后由print
消耗时,对show
的调用会强制对元组进行全面评估,并在此时进行所有比较。
使用seq
代替强制f
来评估当时的比较,从而在执行比较之前评估累加器中包含的thunk。因此结果是存储在累加器中的thunk具有恒定的大小。
foldl'
所做的只是避免构建thunk:f (f (f ...) y) x
。
Jubobs建议的另一种避免明确使用seq
的解决方案是使用具有严格字段的数据类型:
data Pair a b = Pair !a !b
deriving Show
因此代码将成为:
minMax :: [Int] -> Maybe (Pair Int Int)
minMax [] = Nothing
minMax (x:xs) = Just (foldl' f (Pair x x) xs)
where
f (Pair a b) c = Pair (if c < a then c else a) (if c > b then c else b)
这完全避免了砰砰声。
答案 1 :(得分:12)
seq
中使用的foldl'
函数实质上强制将其第一个参数评估为WHNF(弱头范式)。
正如here所解释的那样,WHNF评估会在每次应用构造函数时停止。因此,(a, b)
始终位于WHNF,即使a
和b
是thunk,因为您在访问(,)
之前遇到了元组构造函数a
并且b
。
因此,尽管使用了foldl'
:
foldl' (\ (a, b) x -> (a + x, b + x)) (0, 1) [1..1000000]
但这并不是:
foldl' (\ (a, b) x -> a `seq` b `seq` (a + x, b + x)) (0, 1) [1..10000000]
有时使用-XBangPatterns
扩展程序来编写此代码也很方便:
foldl' (\ (!a, !b) x -> (a + x, b + x)) (0, 1) [1..10000000]