首页 > 解决方案 > 使用 StateT s IO a 的内存泄漏在哪里?

问题描述

意图:学习 Haskell 的小应用程序:下载 wikipedia-article,然后下载从它链接的所有文章,然后下载从它们链接的所有文章,依此类推......直到达到指定的递归深度。结果保存到文件中。

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

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

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

可能的解决方案:

1)函数getNextNode :: StateT CrawlState IO WArticle(下)已经有IO。一种解决方案是只在其中写入文件并仅返回状态。这意味着文件被写入非常小的块。感觉不是很Haskell..

2)具有函数buildHelper :: CrawlState -> IO [WArticle](如下) return [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 个页面。

谢谢你的帮助!

标签: haskelllazy-evaluationstate-monadio-monad

解决方案


2) 在这种情况下,我想要 [IO WArticle] 吗?

不完全的。问题是某些IO WArticle动作取决于先前动作的结果:指向未来页面的链接驻留在先前获得的页面中。[IO Warticle]不能提供:它是纯粹的,你总是可以在列表中找到一个动作而不执行以前的动作。

我们需要的是一种“效果列表”,可以让我们逐条提取文章,逐步执行需要的效果,而不是强迫我们一次性完全生成列表。

有几个库提供这些类型的“有效列表”:管道管道。他们定义了 monad 转换器,这些转换器扩展了基本 monad,能够在返回最终结果之前产生中间值。通常最终结果的类型与产生的值不同;它可能只是 unit ()

注意:这些库的Functor,ApplicativeMonad实例与纯列表的相应实例不同。实例映射到最终的最终值,而不是生成的中间值。为了映射产生的值,它们提供了单独的函数。并且实例对有效列表进行排序,而不是尝试所有组合。为了尝试所有组合,它们提供了单独的功能Functor Monad

使用流媒体库,我们可以修改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_(from Streaming.Prelude,而不是 from Control.Monad!) 之类的函数来处理文章,因为它们是生成的。


推荐阅读