Подтвердить что ты не робот

Поток рекурсивного спуска каталога в Haskell

Я пытаюсь сделать рекурсивный спуск структуры каталогов, используя 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_ в main?

4b9b3361

Ответ 1

Это именно та проблема, с которой были решены итерации/сопрограммы.

Вы можете легко сделать это с помощью pipes. Единственное изменение, которое я внес в ваш getRecursiveContents, заключалось в том, чтобы сделать его Producer из FilePath и respond с именем файла, а не возвращать его. Это позволяет downstream обрабатывать имя файла немедленно, а не ждать завершения 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 вызывает многие проблемы, включая незакрытые ресурсы и выполнение нечистых действий в чистом коде. (См. Также Проблема с ленивым вводом-выводом в Haskell Wiki.)

Безопасный способ - использовать некоторую библиотеку iteratee/enumerator. (Замена проблематичного ленивого ИО была мотивацией для разработки этих понятий.) Ваш getRecursiveContents станет источником данных (перечислитель AKA). И данные будут потребляться некоторым итератором. (См. Также Перечислитель и итерация в вики Haskell.)

Существует учебник по библиотеке перечислений, который просто дает пример перемещения и фильтрации дерева каталогов, реализуя простую утилиту find. Он реализует метод

enumDir :: FilePath -> Enumerator FilePath IO b

который в основном является именно тем, что вам нужно. Я считаю, что вы найдете это интересным.

Также есть хорошая статья, объясняющая итерации в The Monad Reader, выпуск 16: Iteratee: Преподавание старых сфальсифицированных новых трюков от John W Лато, автор библиотеки iteratee.

Сегодня многие люди предпочитают более новые библиотеки, такие как pipes. Вы можете быть заинтересованы в сравнении: Каковы плюсы и минусы Enumerators vs. Conduits vs. Pipes?.

Ответ 3

Благодаря комментарию Никласа Б., вот решение, которое у меня есть:

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, останавливаясь после того, как найду интересующий меня файл. Хотя решения, использующие библиотеки, такие как Enumerator, Conduit и т.д., Кажется, лучшее, что вы могли бы сделать в то время, когда эти ответы были опубликованы, я только что узнал, что IO стал экземпляром Alternative в базовой библиотеке GHC около года назад, что открывает некоторые новые возможности. Вот код, который я написал, чтобы попробовать:

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 (<|>) empty) чтобы продолжать попытки вычислений, пока один из них не достигнет успеха.

Я немного расстраиваюсь, что сигнатура типа действия IO больше не отражает тот факт, что она может преднамеренно не давать результат, но она, несомненно, упрощает код. Раньше я пытался использовать такие типы, как IO (Maybe a), но при этом очень сложно было создавать действия.

IMHO больше нет смысла использовать такой тип, как IO (Maybe a), но если вам нужно взаимодействовать с кодом, который использует такой тип, легко конвертировать между этими двумя типами. Чтобы преобразовать IO a в IO (Maybe a), вы можете просто использовать Control.Applicative.optional, и, идя другим путем, вы можете использовать что-то вроде этого:

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