【问题标题】:Streaming recursive descent of a directory in HaskellHaskell中目录的流式递归下降
【发布时间】:2012-12-24 22:01:16
【问题描述】:

我正在尝试使用 Haskell 对目录结构进行递归下降。我只想根据需要(懒惰地)检索子目录和文件。

我写了以下代码,但是当我运行它时,跟踪显示在第一个文件之前访问了所有目录:

module Main where

import Control.Monad ( forM, forM_, liftM )
import Debug.Trace ( trace )
import System.Directory ( doesDirectoryExist, getDirectoryContents )
import System.Environment ( getArgs )
import System.FilePath ( (</>) )

-- From Real World Haskell, p. 214
getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topPath = do
  names <- getDirectoryContents topPath
  let
    properNames =
      filter (`notElem` [".", ".."]) $
      trace ("Processing " ++ topPath) names
  paths <- forM properNames $ \name -> do
    let path = topPath </> name
    isDirectory <- doesDirectoryExist path
    if isDirectory
      then getRecursiveContents path
      else return [path]
  return (concat paths)

main :: IO ()
main = do
  [path] <- getArgs
  files <- getRecursiveContents path
  forM_ files $ \file -> putStrLn $ "Found file " ++ file

如何将文件处理与下降交错?是files &lt;- getRecursiveContents path动作在main中的下面forM_之前执行的问题吗?

【问题讨论】:

  • Real World Haskell 的"Searching the filesystem" 章节中称为“查看遍历的另一种方式”的后面部分还提供了一种更灵活的文件系统导航方式,该方式使用折叠和迭代器。
  • 我(显然)从 RWH 获取了函数 getRecursiveContents。我没有看到后面的部分。我会看看。谢谢。

标签: lazy-evaluation directory-structure haskell


【解决方案1】:

这正是迭代器/协程旨在解决的问题。

您可以使用pipes 轻松完成此操作。我对您的getRecursiveContents 所做的唯一更改是使其成为FilePaths 的Producerrespond 并使用文件名而不是返回它。这让下游可以立即处理文件名,而不是等待getRecursiveContents 完成。

module Main where

import Control.Monad ( forM_, liftM )
import Control.Proxy
import System.Directory ( doesDirectoryExist, getDirectoryContents )
import System.Environment ( getArgs )
import System.FilePath ( (</>) )

getRecursiveContents :: (Proxy p) => FilePath -> () -> Producer p FilePath IO ()
getRecursiveContents topPath () = runIdentityP $ do
  names <- lift $ getDirectoryContents topPath
  let properNames = filter (`notElem` [".", ".."]) names
  forM_ properNames $ \name -> do
    let path = topPath </> name
    isDirectory <- lift $ doesDirectoryExist path
    if isDirectory
      then getRecursiveContents path ()
      else respond path

main :: IO ()
main = do
    [path] <- getArgs
    runProxy $
            getRecursiveContents path
        >-> useD (\file -> putStrLn $ "Found file " ++ file)

这会在遍历树时立即打印出每个文件,并且不需要惰性IO。更改您对文件名所做的操作也非常容易,因为您所要做的就是使用您的实际文件处理逻辑切换 useD 阶段。

要了解有关pipes 的更多信息,我强烈建议您阅读Control.Proxy.Tutorial

【讨论】:

【解决方案2】:

使用惰性 IO /unsafe...不是的好方法。 Lazy IO 导致many problems,包括未关闭的资源和在纯代码中执行不纯操作。 (另请参阅 Haskell Wiki 上的 The problem with lazy I/O。)

一种安全的方法是使用一些迭代器/枚举器库。 (替换有问题的惰性 IO 是开发这些概念的动机。)您的 getRecursiveContents 将成为数据源(AKA 枚举器)。并且数据将被一些迭代器消耗。 (另请参阅 Haskell wiki 上的 Enumerator and iteratee。)

a tutorial on the enumerator library 只是给出了一个遍历和过滤目录树的例子,实现了一个简单的 find 实用程序。它实现了方法

enumDir :: FilePath -> Enumerator FilePath IO b

这基本上正是您所需要的。相信你会觉得很有趣。

The Monad Reader, Issue 16 中还有一篇很好的文章解释了迭代:Iteratee:教旧折叠新技巧,作者 John W. Lato,iteratee 库的作者。

如今,许多人更喜欢更新的库,例如 pipes。您可能对比较感兴趣:What are the pros and cons of Enumerators vs. Conduits vs. Pipes?

【讨论】:

  • 我已将您提供的所有参考资料添加到我的 Instapaper 帐户中,并将在下班后阅读它们。谢谢。
【解决方案3】:

感谢 Niklas B. 的评论,这是我的解决方案:

module Main where

import Control.Monad ( forM, forM_, liftM )
import Debug.Trace ( trace )
import System.Directory ( doesDirectoryExist, getDirectoryContents )
import System.Environment ( getArgs )
import System.FilePath ( (</>) )
import System.IO.Unsafe ( unsafeInterleaveIO )

-- From Real World Haskell, p. 214
getRecursiveContents :: FilePath -> IO [FilePath]
getRecursiveContents topPath = do
  names <- unsafeInterleaveIO $ getDirectoryContents topPath
  let
    properNames =
      filter (`notElem` [".", ".."]) $
      trace ("Processing " ++ topPath) names
  paths <- forM properNames $ \name -> do
    let path = topPath </> name
    isDirectory <- doesDirectoryExist path
    if isDirectory
      then unsafeInterleaveIO $ getRecursiveContents path
      else return [path]
  return (concat paths)

main :: IO ()
main = do
  [path] <- getArgs
  files <- unsafeInterleaveIO $ getRecursiveContents path
  forM_ files $ \file -> putStrLn $ "Found file " ++ file

有没有更好的办法?

【讨论】:

    【解决方案4】:

    我最近在研究一个非常相似的问题,我正在尝试使用 IO monad 进行一些复杂的搜索,在找到我感兴趣的文件后停止。而使用像 Enumerator 这样的库的解决方案, Conduit 等似乎是发布这些答案时你能做的最好的事情,我刚刚得知 IO 大约一年前在 GHC 的基础库中成为 Alternative 的一个实例,这开辟了一些新的可能性。这是我写的代码来尝试一下:

    import Control.Applicative (empty)
    import Data.Foldable (asum)
    import Data.List (isSuffixOf)
    import System.Directory (doesDirectoryExist, listDirectory)
    import System.FilePath ((</>))
    
    searchFiles :: (FilePath -> IO a) -> FilePath -> IO a
    searchFiles f fp = do
        isDir <- doesDirectoryExist fp
        if isDir
            then do
                entries <- listDirectory fp
                asum $ map (searchFiles f . (fp </>)) entries
            else f fp
    
    matchFile :: String -> FilePath -> IO ()
    matchFile name fp
        | name `isSuffixOf` fp = putStrLn $ "Found " ++ fp
        | otherwise = empty
    

    searchFiles 函数对目录树进行深度优先搜索,当它找到您要查找的内容时停止,这取决于作为第一个参数传递的函数。 matchFile 函数只是用来展示如何构造一个合适的函数用作searchFiles 的第一个参数;在现实生活中,您可能会做一些更复杂的事情。

    这里有趣的是,现在您可以使用empty 使IO 计算“放弃”而不返回结果,并且您可以将计算与asum 链接在一起(这只是foldr (&lt;|&gt;) empty)继续尝试计算,直到其中一个成功。

    我发现 IO 操作的类型签名不再反映它可能故意不产生结果的事实有点令人不安,但它确实简化了代码。我之前尝试使用IO (Maybe a) 之类的类型,但这样做会导致很难编写动作。

    恕我直言,没有太多理由使用像IO (Maybe a) 这样的类型,但是如果您需要与使用这种类型的代码进行交互,那么在这两种类型之间进行转换很容易。要将IO a 转换为IO (Maybe a),您可以使用Control.Applicative.optional,反之,您可以使用以下代码:

    maybeEmpty :: IO (Maybe a) -> IO a
    maybeEmpty m = m >>= maybe empty pure
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-03-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多