Haskell函数参数力评估

时间:2015-10-09 22:20:02

标签: haskell

我需要从列表中获取最后n个元素,使用O(n)内存,所以我写了这段代码

take' :: Int -> [Int] -> [Int]
take' n xs = (helper $! (length $! xs) - n + 1) xs
    where helper skip [] = []
          helper skip (x : xs) = if skip == 0 then xs else (helper $! skip - 1) xs

main = print (take' 10 [1 .. 100000])

此代码需要O(|L|)内存,其中|L| - 是给定列表的长度。

但是当我写这段代码时

take' :: Int -> [Int] -> [Int]
take' n xs = helper  (100000 - n + 1) xs
    where helper skip [] = []
          helper skip (x : xs) = if skip == 0 then xs else (helper $! skip - 1) xs


main = print (take' 10 [1 .. 100000])

此代码现在只占用O(n)个内存(唯一的chage是(helper $! (length $! xs) - n + 1) - > helper (100000 - n + 1)

所以,据我所知,Haskell由于某种原因在第一次调用length xs之前没有评估helper所以它在skip中留下了一个thunk并且haskell必须保留这个值在每个堆栈帧中而不是进行尾递归。但在第二段代码中,它会评估(100000 - n + 1)并将纯值提供给helper

所以问题是如何在第一次调用helper之前评估列表的长度,并且只使用O(n)内存。

2 个答案:

答案 0 :(得分:8)

other answer提到成为一个好消费者意味着什么。您已经发布了两个版本的函数,一个适用于任意长度的列表,但不是一个好的消费者,一个是一个很好的消费者,但假定一个特定的列表长度。为了完整性,这里有一个功能很好的消费者,适用于任意列表长度:

takeLast n xs = go (drop n xs) xs where
    go (_:xs) (_:ys) = go xs ys
    go _ ys = ys

答案 1 :(得分:5)

第二个版本实际上并不只占用 O n )内存。无论take'做什么:你从一个长度为 L 的列表开始,并且必须存储在某个地方。

有效 O n )内存的原因是该列表仅供一位“好消费者”使用,即helper。这样的消费者从头到尾解构清单;因为在其他任何地方都不需要对头部的引用,垃圾收集器可以立即开始清理那些第一个元素 - 在列表理解之前甚至已经建立了列表的其余部分!

但是,如果在使用helper之前,您会计算该列表的length。这已经强制整个列表为NF'd ,正如我所说,这不可避免地需要 O L )内存。因为你仍然持有一个与helper一起使用的引用,在这种情况下,垃圾收集器可以在整个列表在内存之前采取任何行动。

所以,它与严格的评估无关。事实上,实现目标的唯一方法是使 less 严格(只需要在任何给定时间评估长度 n 的子列表)。

更准确地说:它强制列表的 spine 为普通形式。不评估元素,但它仍然 O L )。