在haskell中严格执行问题
如果想假装Haskell是严格的,并且我有一个不利用懒惰的算法(例如它不使用无限列表),如果我仅使用严格的数据类型并注释了我使用的任何函数,会发生什么问题,在其论据中要严格? 是否会有性能损失,如果有的话会有多糟糕; 会发生更糟的问题吗? 我知道这是肮脏,毫无意义和丑陋,无意识地使每个函数和数据类型严格,我不打算在实践中这样做,但我只想明白,如果这样做,Haskell默认情况下变得严格吗?
其次,如果我淡化偏执狂,并且只对数据结构进行严格的限制:只有当我使用某种形式的积累时,我才不得不担心懒惰实现带来的空间泄漏? 换句话说,假设该算法不会以严格的语言显示空间泄漏。 还假设我在Haskell中只使用严格的数据结构实现它,但是小心地使用seq来评估递归中传递的任何变量,或者使用内部小心这样做的函数(如fold)我避免了任何空间泄漏? 请记住,我假设在严格的语言中,相同的算法不会导致空间泄漏。 所以这是一个关于懒惰和严格之间实现差异的问题。
我问第二个问题的原因是因为除了使用懒惰的数据结构或者严格的数据结构试图利用懒惰的情况外,我所见过的所有空间泄漏示例都只涉及到thunk在累加器中进行开发,因为它不是被递归调用的函数在应用它之前没有对累加器进行评估。 我知道,如果想要利用懒惰,那么必须格外小心,但是在严格的默认语言中也需要谨慎。
谢谢。
懒惰加快速度
你可能会变得更糟。 ++
的天真定义是:
xs ++ ys = case xs of (x:xs) -> x : (xs ++ ys)
[] -> ys
懒惰使得这个O(1),尽管它也可以增加O(1)处理来提取缺点。 没有懒惰, ++
需要立即进行评估,导致O(n)操作。 (如果你从未见过O(。)表示法,那么这是计算机科学从工程师窃取的东西:给定一个函数f
,集合O( f(n) )
是所有算法的集合,它们最终处于最坏状态 -正比于f(n)
,其中n
是输入给函数的输入比特数[正式地,存在k
和N
,使得对于所有n > N
,算法花费的时间小于k * f(n)
。 ]所以我说laziness使得上面的操作O(1)
或者最终是恒定的,但是为每个提取增加了一个不变的开销,而严格性使得操作O(n)
或者最终在列表元素的数量上是线性的,假设这些元素具有固定的大小。
这里有一些实际的例子,但是O(1)增加的处理时间也可能“叠加”成O(n)依赖性,所以最明显的例子是O(n2)两种方式。 这些例子仍然存在差异。 例如,一种情况不太好,就是使用队列(先进先出)的堆栈(后进先出,这是Haskell列表的样式)。
所以这里有一个由严格的左褶皱组成的快速库; 我使用了case语句,以便每行都可以粘贴到GHCi中(使用let
):
data SL a = Nil | Cons a !(SL a) deriving (Ord, Eq, Show)
slfoldl' f acc xs = case xs of Nil -> acc; Cons x xs' -> let acc' = f acc x in acc' `seq` slfoldl' f acc' xs'
foldl' f acc xs = case xs of [] -> acc; x : xs' -> let acc' = f acc x in acc' `seq` foldl' f acc' xs'
slappend xs ys = case xs of Nil -> ys; Cons x xs' -> Cons x (slappend xs' ys)
sl_test n = foldr Cons Nil [1..n]
test n = [1..n]
sl_enqueue xs x = slappend xs (Cons x Nil)
sl_queue = slfoldl' sl_enqueue Nil
enqueue xs x = xs ++ [x]
queue = foldl' enqueue []
这里的诀窍是queue
和sl_queue
遵循xs ++ [x]
模式来追加一个元素到列表的末尾,该列表需要一个列表并构建该列表的精确副本。 GHCi然后可以运行一些简单的测试。 首先,我们制作两件东西,并强迫他们证明这一操作本身速度很快,并且在内存中的成本并不太高:
*Main> :set +s
*Main> let vec = test 10000; slvec = sl_test 10000
(0.02 secs, 0 bytes)
*Main> [foldl' (+) 0 vec, slfoldl' (+) 0 slvec]
[50005000,50005000]
(0.02 secs, 8604632 bytes)
现在我们进行实际测试:总结队列版本:
*Main> slfoldl' (+) 0 $ sl_queue slvec
50005000
(22.67 secs, 13427484144 bytes)
*Main> foldl' (+) 0 $ queue vec
50005000
(1.90 secs, 4442813784 bytes)
请注意,这两种方式在内存性能方面都很糟糕(列表附加内容仍然是秘密的O(n2)),它们最终占据了千兆字节的空间,但严格版本占用了三倍的空间,并且占用了十倍的时间。
有时数据结构应该改变
如果你真的想要一个严格的队列,有几个选项。 一种是像Data.Sequence
手指树 - 他们做事情的viewr
方式有点复杂,但是可以用来获取最右边的元素。 然而,这有点沉重,一个常见的解决方案是O(1)摊销:定义结构
data Queue x = Queue !(SL x) !(SL x)
SL
条款是上面严格的条款。 定义一个严格的reverse
,让我们称之为slreverse
,显而易见的方法,然后考虑:
enqueue :: Queue x -> x -> Queue x
enqueue (Queue xs ys) el = Queue xs (Cons el ys)
dequeue :: Queue x -> Maybe (x, Queue x)
dequeue (Queue Nil Nil) = Nothing
dequeue (Queue Nil (Cons x xs)) = Just (x, Queue (slreverse xs) Nil)
dequeue (Queue (Cons x xs ys)) = Just (x, Queue xs ys)
这是“摊销O(1)”:每当dequeue
列列表反转列表,对于某个k
花费O(k)个步骤,我们确保我们创建的结构不必为k
更多步骤支付这些成本。
懒惰隐藏错误
另一个有趣的点来自数据/数据库的区别,其中数据是由亚单位递归遍历的有限结构(即每个数据表达式都停止),而数据库是结构的其余部分 - 严格的列表与流。 事实证明,当你正确地做出这种区分时,严格数据和懒惰数据之间没有形式上的区别 - 严格和懒惰之间唯一的形式区别在于它们如何处理无限循环的术语:strict将探索循环,因此也会无限循环,而懒惰只会将无限循环向前而不下降。
因此,您会发现x = slhead (Cons x undefined)
在head (x : undefined)
成功的情况下将失败。 所以当你这样做时,你可能会“发现”隐藏的无限循环或者错误。
谨慎制作“一切都很严格”
当你在你的语言中使用严格的数据结构时,并非所有东西都会变得严格:注意,我在上面提到了为严格列表和严格列表定义严格的foldl
而不是foldl
。 Haskell中的通用数据结构将是懒惰的 - 列表,元组,常用库中的东西 - 以及在构建复杂表达式时显式调用seq
仍然有帮助。
上一篇: Problems with enforcing strictness in haskell
下一篇: What is the point of having a lazy/strict version of Writer?