使用 StateT s IO a 时的内存泄漏在哪里?



意图:学习的小应用程序 哈斯克尔:下载维基百科文章,然后下载从它链接的所有文章,然后下载从它们链接的所有文章,依此类推......直到达到指定的递归深度。结果将保存到文件中。

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

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

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

可能的解决方案:

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

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( 在这种情况下,[IO WArticle] 想要我想要吗?

差一点。问题在于,某些IO WArticle操作取决于上一个操作的结果:指向未来页面的链接驻留在以前获得的页面中。[IO Warticle]无法提供这一点:从某种意义上说,它是纯粹的,您始终可以在列表中找到操作而无需执行先前的操作。

我们需要的是一种"有效列表",让我们逐一提取文章,逐步执行必要的效果,但又不强迫我们一次性完全生成列表。

有几个库提供这些类型的"有效列表":流、管道、导管。他们定义了单子变压器,这些变压器扩展了基本单子,能够在返回最终结果之前产生中间值。通常,最终结果的类型与生成的值不同;它可能只是单位().

注意:这些库的FunctorApplicativeMonad实例与纯列表的相应实例不同。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_这样的函数(来自Streaming.Prelude,而不是来自Control.Monad!(来逐个处理文章,因为它们是生成的。

在 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获取字符串流并将它们写入指定的句柄。

S.show地图show :: WArticle -> String流。

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

最新更新