Haskell 在不牺牲性能的情况下避免折叠中的堆栈溢出

标签 haskell fold bytestring

以下代码在大输入时会发生堆栈溢出:

{-# LANGUAGE DeriveDataTypeable, OverloadedStrings #-}
import qualified Data.ByteString.Lazy.Char8 as L


genTweets :: L.ByteString -> L.ByteString
genTweets text | L.null text = ""
               | otherwise = L.intercalate "\n\n" $ genTweets' $ L.words text
  where genTweets' txt = foldr p [] txt
          where p word [] = [word]
                p word words@(w:ws) | L.length word + L.length w <= 139 =
                                        (word `L.append` " " `L.append` w):ws
                                    | otherwise = word:words

我假设我的谓词正在构建一个 thunk 列表,但我不确定为什么,或者如何修复它。

使用 foldl' 的等效代码运行良好,但需要很长时间,因为它不断附加,并使用大量内存。

import Data.List (foldl')

genTweetsStrict :: L.ByteString -> L.ByteString
genTweetsStrict text | L.null text = "" 
                     | otherwise = L.intercalate "\n\n" $ genTweetsStrict' $ L.words text
  where genTweetsStrict' txt = foldl' p [] txt
          where p [] word = [word]
                p words word | L.length word + L.length (last words) <= 139 =
                                init words ++ [last words `L.append` " " `L.append` word]
                             | otherwise = words ++ [word]

是什么导致第一个片段产生重击,可以避免吗?是否可以编写第二个片段,使其不依赖于 (++)

最佳答案

L.length word + L.length (last words) <= 139

这就是问题所在。在每次迭代中,您都会遍历累加器列表,然后

init words ++ [last words `L.append` " " `L.append` word]

附加在最后。显然这将花费很长时间(与累加器列表的长度成正比)。更好的解决方案是延迟生成输出列表,将处理与读取输入流交错(您不需要读取整个输入来输出第一个 140 个字符的推文)。

以下版本的程序在使用 O(1) 空间的情况下,在 1 秒内处理相对较大的文件 (/usr/share/dict/words):

{-# LANGUAGE OverloadedStrings, BangPatterns #-}

module Main where

import qualified Data.ByteString.Lazy.Char8 as L
import Data.Int (Int64)

genTweets :: L.ByteString -> L.ByteString
genTweets text | L.null text = ""
               | otherwise   = L.intercalate "\n\n" $ toTweets $ L.words text
  where

    -- Concatenate words into 139-character tweets.
    toTweets :: [L.ByteString] -> [L.ByteString]
    toTweets []     = []
    toTweets [w]    = [w]
    toTweets (w:ws) = go (L.length w, w) ws

    -- Main loop. Notice how the output tweet (cur_str) is generated as soon as
    -- possible, thus enabling L.writeFile to consume it before the whole
    -- input is processed.
    go :: (Int64, L.ByteString) -> [L.ByteString] -> [L.ByteString]
    go (_cur_len, !cur_str) []     = [cur_str]
    go (!cur_len, !cur_str) (w:ws)
      | lw + cur_len <= 139        = go (cur_len + lw + 1,
                                         cur_str `L.append` " " `L.append` w) ws
      | otherwise                  = cur_str : go (lw, w) ws
      where
        lw = L.length w

-- Notice the use of lazy I/O.
main :: IO ()
main = do dict <- L.readFile "/usr/share/dict/words"
          L.writeFile "tweets" (genTweets dict)

关于Haskell 在不牺牲性能的情况下避免折叠中的堆栈溢出,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18601033/

相关文章:

haskell - Haskell 中的内部数据结构(文本)

haskell - 在 Haskell 中折叠,使用多个函数

Haskell 函数参数力评估

Haskell 函数声明

haskell - Haskell 中字节流的高效流式传输和操作

git - 从 Haskell 中的 git packfile index 获取魔数(Magic Number)

list - 如何过滤两个输入列表的元组列表?

scala - Fold 和 FoldLeft 或 FoldRight 之间的区别?

scala - 在 scala 中使用 reduceLeft 代替 foldLeft

java - 应该如何连接数组流?