如果想假装Haskell是严格的并且我有一个算法,不利用懒惰(例如它不使用无限列表),如果我只使用,会出现什么问题严格的数据类型和注释我使用的任何函数,在其参数中是严格的吗?会不会有性能损失,如果有的话会有多糟糕;会发生更糟糕的问题吗我知道它是脏的,毫无意义和丑陋的,无意识地使每个函数和数据类型都严格,我不打算在实践中这样做,但我只是想了解如果通过这样做,Haskell默认变得严格?
其次,如果我淡化偏执狂,只会使数据结构严格:只有在我使用某种形式的积累时,我才会担心由懒惰实现引起的空间泄漏 ?换句话说,假设算法不会以严格的语言表现出空间泄漏。还假设我使用 only 严格的数据结构在Haskell中实现了它,但是小心地使用seq来评估在递归中传递的任何变量,或者使用内部谨慎执行的函数(像折叠'),我会避免任何空间泄漏?请记住,我假设在严格的语言中,相同的算法不会导致空间泄漏。所以这是一个关于lazy和strict之间的实现差异的问题。
我问第二个问题的原因是因为除了一个人试图通过使用懒惰的数据结构或脊椎严格的数据结构来利用懒惰的情况之外,我所看到的所有空间泄漏的例子,只涉及在累加器中开发的thunk,因为它不是递归调用的函数,而是在对其应用之前不评估累加器。我知道如果一个人想要利用懒惰,那么必须要格外小心,但是在严格的默认语言中也需要谨慎。
谢谢。
答案 0 :(得分:3)
你可能更糟糕。 ++
的天真定义是:
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
和{{1这样,对于所有N
,算法花费的时间少于n > N
。]所以我说懒惰使上面的操作k * f(n)
或最终成为恒定时间,但是为每次提取添加了一个恒定的开销,而严格性使得操作O(1)
或最终在列表元素的数量中是线性的,假设这些元素具有固定的大小。
这里有一些实际的例子,但O(1)增加的处理时间也可能“堆积”成O(n)依赖,所以最明显的例子是O(n 2 ) 双向。这些例子仍然存在差异。例如,一种不能正常工作的情况是使用堆栈(后进先出,这是Haskell列表的样式)用于队列(先进先出)。
所以这是一个由严格的左褶皱组成的快速库;我使用了case语句,这样每行都可以粘贴到GHCi中(带O(n)
):
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
模式将一个元素附加到列表的末尾,该列表采用一个列表并构建一个精确的副本那份清单。然后GHCi可以运行一些简单的测试。首先,我们制作两个项目并强制他们的thunk来证明这个操作本身非常快,并且太在内存中过于昂贵:
xs ++ [x]
现在我们进行实际测试:总结队列版本:
*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)
请注意,这两个 suck 在内存性能方面(list-append的东西仍然是秘密的O(n 2 )),最终它们占用了几千兆字节空间,但严格的版本占用了三倍的空间,占用了十倍的时间。
如果你真的想要一个严格的队列,有几个选择。一个是*Main> slfoldl' (+) 0 $ sl_queue slvec
50005000
(22.67 secs, 13427484144 bytes)
*Main> foldl' (+) 0 $ queue vec
50005000
(1.90 secs, 4442813784 bytes)
中的手指树 - 他们做事的Data.Sequence
方式有点复杂,但却能找到最合适的元素。然而,这有点沉重,一个常见的解决方案是O(1)摊销:定义结构
viewr
其中data Queue x = Queue !(SL x) !(SL x)
项是上面的严格堆栈。定义一个严格的SL
,让它称之为reverse
,显而易见,然后考虑:
slreverse
这是“摊销O(1)”:每次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(k)步骤为某些dequeue
,我们确保我们创建一个赢得的结构不必为k
个步骤支付这些费用。
另一个有趣的观点来自数据/ codata的区别,其中数据是由子单元递归遍历的有限结构(即每个数据表达式停止),而codata是结构的其余部分 - 严格列表与流。事实证明,当你正确地进行这种区分时,严格数据和惰性数据之间没有没有形式差异 - 严格和懒惰之间唯一的形式差异在于它们如何处理自身内无条件循环的术语:严格将探索循环,因此也将无限循环,而懒惰将简单地向前移动无限循环而不会下降到它。
因此,您会发现k
在x = slhead (Cons x undefined)
成功的地方失败。因此,当您这样做时,您可能会“发现”隐藏的无限循环或错误。
当您在您的语言中使用严格的数据结构时,并非所有内容都必须严格:请注意,我在上面列出了为列表和严格列表定义严格head (x : undefined)
而非foldl
的内容。 Haskell中的常见数据结构将是惰性的 - 列表,元组,流行库中的东西 - 以及对foldl
的显式调用在构建复杂表达式时仍然有用。