11

我正在尝试使用 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 <- getRecursiveContents path在以下forM_in之前执行该操作main吗?

4

4 回答 4

9

这正是 iteratees/coroutines 旨在解决的问题。

您可以使用pipes. 我对您所做的唯一更改getRecursiveContents是将其设为 a Producerof FilePaths 并respond使用文件名而不是返回它。这让下游立即处理文件名,而不是等待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)

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

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

于 2013-01-10T15:46:49.657 回答
7

使用惰性 IO /unsafe...不是一个好方法。Lazy IO 会导致很多问题,包括未关闭的资源和在纯代码中执行不纯的操作。(另请参阅Haskell Wiki 上的惰性 I/O 问题。)

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

有一个关于枚举器库的教程,它只是给出了一个遍历和过滤目录树的例子,实现了一个简单的查找实用程序。它实现方法

enumDir :: FilePath -> Enumerator FilePath IO b

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

在The Monad Reader, Issue 16中还有一篇很好的文章解释了 iteratee :Iteratee: Teaching an Old Fold New Tricks by John W. Lato,iteratee库的作者。

今天,许多人更喜欢新的库,例如管道。您可能对比较感兴趣:枚举器与导管与管道的优缺点是什么?.

于 2013-01-10T14:49:58.697 回答
2

感谢 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

有没有更好的办法?

于 2013-01-10T14:17:50.330 回答
0

我最近在研究一个非常相似的问题,我正在尝试使用IOmonad 进行一些复杂的搜索,在找到我感兴趣的文件后停止。虽然使用 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; 的第一个参数。在现实生活中,您可能会做一些更复杂的事情。

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

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

恕我直言,不再有理由使用类似的类型IO (Maybe a),但是如果您需要与使用类似类型的代码进行交互,则很容易在两种类型之间进行转换。要转换IO aIO (Maybe a),您可以使用Control.Applicative.optional,反之,您可以使用以下内容:

maybeEmpty :: IO (Maybe a) -> IO a
maybeEmpty m = m >>= maybe empty pure
于 2017-10-20T00:00:28.183 回答