【问题标题】:Haskell function parameter force evaluationHaskell 函数参数力评估
【发布时间】:2015-10-09 22:20:02
【问题描述】:

我需要从列表中取出最后一个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) 内存(唯一的变化是(helper $! (length $! xs) - n + 1) -> helper (100000 - n + 1)

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

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

【问题讨论】:

  • 有一个“众所周知的”技巧可以做到这一点,我不希望你在没有提示的情况下猜到。提示:skip 参数使用列表而不是数字。
  • @ØrjanJohansen 似乎解决方案是最后的答案。无论如何感谢您的提示 =)

标签: haskell


【解决方案1】:

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

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

【讨论】:

  • 您可以通过删除第一个分支并将第三个分支替换为go _ ys = ys来避免不可能的错误。
  • @dfeuer 不错!已更新。
  • 哦,非常漂亮的解决方案。我不这么认为)
【解决方案2】:

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

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

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

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


更准确地说:它强制列表的 spine 为正常形式。元素没有被评估,但它仍然是 O(L)。

【讨论】:

  • 是的,当我谈到内存时,我指的是bytes maximum residency 字段。非常感谢您的回答,知道什么是真正的错误,我可以考虑如何解决它:)
  • 这个“好消费者”的意思不是one used in the GHC documentation——它指的是一个消费者可以在编译时融合一个列表参数本身就是一个“好制作人”。这比目前的情况更有效,这只是基本的懒惰。
  • @Ørjan Johansen 说得对,但懒惰的解构几乎不是一个好的消费者的要求吗?
  • 也许吧。我担心即使使用惰性解构,您也可能将流控制过于复杂而无法有效地转化为foldr 的使用。虽然我没有例子。