Поток рекурсивного спуска каталога в 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
?
Ответы
Ответ 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