我可以利用惰性求值来引用未来值而不会出现空间泄漏吗?
我希望尝试在大量输入上运行一个适度昂贵的函数,使用该函数的部分输出作为其输入之一。代码按预期运行,不幸的是它在进程中消耗了大量内存(堆上不到 22GiB,最大驻留时间刚好超过 1GiB)。这是我的意思的简化示例:
{-# LANGUAGE OverloadedStrings #-}
import Data.List (foldl')
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.IO as TL
import qualified Data.Text.Lazy.Builder as TB
main :: IO ()
main = TL.putStr $ TB.toLazyText showInts
showInts :: TB.Builder
showInts = foldMap fst shownLines
where
shownLines = map (showInt maxwidth) [0..10^7]
maxwidth = foldl' (n -> max n . snd) 0 shownLines
showInt :: Int -> Int -> (TB.Builder, Int)
showInt maxwidth n = (builder, len)
where
builder = TB.fromText "This number: "
<> TB.fromText (T.replicate (maxwidth - len) " ") <> thisText
<> TB.singleton 'n'
(thisText, len) = expensiveShow n
expensiveShow :: Int -> (TB.Builder, Int)
expensiveShow n = (TB.fromText text, T.length text)
where text = T.pack (show n)
请注意,在 , 的 where 子句中showInts,showInt将maxwidth作为参数, wheremaxwidth本身取决于在showInt maxwidth整个列表上运行的输出。
如果,在另一方面,我做的奈?事情已经和替换的定义maxwidth有foldl' max 0 $ map (snd . expensiveShow) [0..10^7],那么最大的居住下降到刚刚44KiB。我希望这样的性能可以在没有预计算之类的解决方法的情况下实现expensiveShow,然后将其与列表一起压缩[0..10^7]。
我尝试严格使用列表(使用foldl包),但这并没有改善情况。
我正在尝试拥有我的蛋糕并也吃它:利用懒惰,同时也使事情变得足够严格,以至于我们不会堆积如山。这是可能的吗?或者有没有更好的技术来实现这一点?
回答
你不能这样做。
问题是您showInts必须遍历列表两次,第一次找到最长的数字,第二次以必要的格式打印数字。这意味着列表必须在第一遍和第二遍之间保存在内存中。对于未评估的 thunk,这不是问题;只是整个列表,完全评估,被遍历两次。
唯一的解决方案是两次生成相同的列表。在这种情况下,这是微不足道的;只需有两个[0..10^7]值,一个用于最大长度,第二个用于格式化它们。我怀疑在您的实际应用程序中,您正在从文件或其他内容中读取它们,在这种情况下,您需要读取该文件两次。