使用StateT的IO a在哪里发生内存泄漏?

时间:2018-06-24 20:41:00

标签: haskell lazy-evaluation state-monad io-monad

意图:学习Haskell的小型应用程序:下载维基百科文章,然后下载与其链接的所有文章,然后下载与其链接的所有文章,依此类推...直到指定的递归达到深度。结果保存到文件中。

方法:使用StateT跟踪下载队列,下载文章并更新队列。我递归建立一个列表IO [WArticle],然后打印它。

问题:分析过程中,我发现正在使用的总内存与下载的文章数量成正比。

分析:根据文献,我认为这是一个懒惰和/或严格性问题。 BangPatterns减少了内存消耗,但没有解决比例问题。此外,我知道所有文章都是在文件输出开始之前下载的。

可能的解决方案:

1)函数getNextNode :: StateT CrawlState IO WArticle(如下)已经具有IO。一种解决方案是只在其中写入文件,然后仅返回状态。但这意味着将文件写入很小的块中。感觉不太Haskell。

2)使函数buildHelper :: CrawlState -> IO [WArticle](在下面)返回[IO WArticle]。虽然我不知道如何重写该代码,并且已在注释中建议不要这样做。

这些提议的解决方案中有没有一个比我认为的要好?或者有更好的替代方案?

import GetArticle (WArticle, getArticle, wa_links, wiki2File) -- my own
type URL = Text

data CrawlState =
     CrawlState  ![URL]       ![(URL, Int)]
          --    [Completed]    [(Queue, depth)]
-- Called by user
buildDB :: URL -> Int -> IO [WArticle]
buildDB startURL recursionDepth = buildHelper cs
    where cs = CrawlState [] [(startURL, recursionDepth)]

-- Builds list recursively
buildHelper :: CrawlState -> IO [WArticle]
buildHelper !cs@(CrawlState _ queue) = {-# SCC "buildHelper" #-}
  if null queue
    then return []
    else do
      (!article, !cs') <- runStateT getNextNode cs
      rest <- buildHelper cs'
      return (article:rest)

-- State manipulation
getNextNode :: StateT CrawlState IO WArticle
getNextNode = {-# SCC "getNextNode" #-} do
  CrawlState !parsed !queue@( (url, depth):queueTail ) <- get
  article <- liftIO $ getArticle url
  put $ CrawlState (url:parsed) (queueTail++ ( if depth > 1
          then let  !newUrls  = wa_links article \\ parsed
                    !newUrls' = newUrls          \\ map fst queue
                    in zip newUrls' (repeat (depth-1))
          else []))
  return article

startUrl = pack "https://en.wikipedia.org/wiki/Haskell_(programming_language)"
recursionDepth = 3

main :: IO ()
main =  {-# SCC "DbMain" #-}
  buildDB startUrl recursionDepth
   >>= return . wiki2File
   >>= writeFile "savedArticles.txt"

位于https://gitlab.com/mattias.br/sillyWikipediaSpider的完整代码。当前版本仅限于从每个页面下载前八个链接以节省时间。无需更改即可下载约600 MB堆使用量的55页。

感谢您的帮助!

2 个答案:

答案 0 :(得分:1)

  

2)在这种情况下[IO WArticle]是否要我提供?

不完全是。问题在于某些IO WArticle动作取决于上一个动作的结果:指向将来页面的链接位于先前获取的页面中。 [IO Warticle]不能提供这一点:纯粹是在您始终可以在列表中找到一个动作而不执行先前的动作的意义上。

我们需要的是一种“有效列表”,它使我们可以逐个提取文章,逐步执行必要的效果,但又不强迫我们一次完成生成列表。

有几种提供这些“有效列表”的库:streamingpipesconduit。他们定义了monad转换器,这些转换器可以扩展基本monad并具有yield中间值的能力,然后返回最终结果。通常,最终结果的类型不同于所产生的值。可能只是单位()

注意:这些库的FunctorApplicativeMonad实例与纯列表的相应实例不同。 Functor instances映射到结果最终值上,而不是生成的中间值上。为了映射产生的值,它们提供了separate functions。并且Monad实例 sequence 有效列表,而不是尝试所有组合。要尝试所有组合,请提供separate functions

使用streaming库,我们可以将buildHelper修改为如下形式:

import Streaming
import qualified Streaming.Prelude as S

buildHelper :: CrawlState -> Stream (Of WArticle) IO ()
buildHelper !cs@(CrawlState _ queue) = 
  if null queue
    then return []
    else do (article, cs') <- liftIO (runStateT getNextNode cs)
            S.yield article
            buildHelper cs'

然后我们可以使用mapM_之类的功能(来自Streaming.Prelude,而不是Control.Monad中的功能!)在生成文章时一一处理。

答案 1 :(得分:0)

根据danidiaz的回答添加进一步的解释和代码构建。这是最终的代码:

import Streaming
import qualified Streaming.Prelude as S
import System.IO (IOMode (WriteMode), hClose, openFile)

buildHelper :: CrawlState -> Stream (Of WArticle) IO ()
buildHelper cs@( CrawlState _ queue ) = 
  if null queue
    then return ()
    else do
      (article, cs') <- liftIO (runStateT getNextNode cs)
      S.yield article
      buildHelper cs'

main :: IO ()
main = do outFileHandle <- openFile filename WriteMode
          S.toHandle outFileHandle  . S.show . buildHelper $
              CrawlState [] [(startUrl, recursionDepth)]
          hClose outFileHandle

outFileHandle是通常的文件输出句柄。

S.toHandle接受String流,并将它们写入指定的句柄。

S.show在流上映射show :: WArticle -> String

一种优雅的解决方案,即使它是由一系列IO操作(即下载网站)产生的,也会创建一个惰性流,并在结果可用时将其写入文件中。在我的机器上,它在执行期间仍然使用大量内存(相对于任务),但从未超过450 MB。