Распечатка всего содержимого каталога по порядку в ширину приводит к низкой эффективности

Я написал модуль Haskell, чтобы вывести все содержимое каталога в порядке убывания. Ниже приведен исходный код.

module DirElements (dirElem) where

import System.Directory (getDirectoryContents, doesDirectoryExist)
import System.FilePath ((</>))

dirElem :: FilePath -> IO [[FilePath]]
dirElem dirPath = iterateM (not.null) (concatMapM getDirectoryContents') [dirPath] >>= return.tail

getDirectoryContents' :: FilePath -> IO [FilePath]
getDirectoryContents' dirPath = do
  isDir <- do doesDirectoryExist dirPath
  if isDir then dirContent else return [] where
    dirContent = do
      contents <- getDirectoryContents dirPath
      return.(map (dirPath</>)).tail.tail $ contents

iterateM :: (Monad m) => (a -> Bool) -> (a -> m a) -> a -> m [a]
iterateM fb f x = do --Notice: Due to the the implementation of >>=, iterateM can't be writen like iterate which gives a infinite list and have type of iterateM :: (Monad m) => (a -> Bool) -> (a -> m a) -> a -> m [a]
  if fb x
    then do
      tail <- do {fx <- f x; iterateM fb f fx}
      return (x:tail)
    else return []

concatMapM :: Monad m => (a -> m[b]) -> [a] -> m[b]
concatMapM f list = mapM f list >>= return.concat

Он работает правильно, но при работе с большим каталогом он на некоторое время "приостанавливается" и выдает все результаты.

После исследования я обнаружил, что это тот же вопрос, sequence $ map return [1..]::[[Int]] посмотрите, почему функция последовательности Haskell не может быть ленивой или почему рекурсивные монадические функции не могут быть ленивыми

4 ответа

Решение

Я изменил старый ответ, с которым Даворак связался, чтобы использовать новый pipes библиотека.

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

import Control.Monad
import Control.Proxy
import Control.Proxy.Trans.Maybe
import Control.Proxy.Trans.State as S
import Data.Sequence hiding (filter)
import System.FilePath.Posix
import System.Directory

getUsefulContents :: FilePath -> IO [FilePath]
getUsefulContents path
  = fmap (filter (`notElem` [".", ".."])) $ getDirectoryContents path

traverseTree
    :: (Proxy p)
    => FilePath
    -> () -> Producer (MaybeP (StateP (Seq FilePath) p)) FilePath IO r
traverseTree path () = do
    liftP $ S.modify (|> path)
    forever $ do
        x <- liftP $ S.gets viewl
        case x of
            EmptyL    -> mzero
            file :< s -> do
                liftP $ S.put s
                respond file
                p <- lift $ doesDirectoryExist file
                when p $ do
                    names <- lift $ getUsefulContents file
                    let namesfull = map (file </>) names
                    liftP $ forM_ namesfull $ \name ->
                        S.modify (|> name)

Это определяет ленивого производителя файлов в ширину. Если вы подключите его к стадии печати, он будет распечатывать файлы по мере прохождения дерева:

main = runProxy $ evalStateK empty $ runMaybeK $
    traverseTree "/tmp" >-> putStrLnD

Лень означает, что если вам требуется только 3 файла, он будет проходить по дереву столько, сколько необходимо для генерации трех файлов, тогда он остановится:

    main = runProxy $ evalStateK empty $ runMaybeK $
        traverseTree "/tmp" >-> takeB_ 3 >-> putStrLnD

Если вы хотите узнать больше о pipes библиотека, тогда я рекомендую вам прочитать учебник.

Это происходит время от времени, и в итоге ответом будет использование библиотеки, подобной повторяющейся. В последнее время чаще всего предлагается библиотека Proxy.

Я уже видел решения Conduit и несколько элегантных монадических решений, но сейчас я их не нахожу.

Прежде всего, это не связано со строгостью. Как и многие монады, IO на самом деле не строг в своих монадических операциях. Это связано с ленивым или нетерпеливым вводом / выводом.

Проблема в том, что вы сначала выполняете обход каталога, а затем обрабатываете результат. Вы можете улучшить это, используя сопрограммы для их чередования. Один простой способ - заставить обход каталога принимать обратный вызов в качестве аргумента:

getDirectoryContents' :: (MonadIO m) => (FilePath -> m a) -> FilePath -> m ()
getDirectoryContents' k fp = {- ... -}

Это самое простое и наименее гибкое решение. Более гибким решением является реализация сопрограмм. Вы можете либо свернуть свою собственную монаду сопрограмм, используя свободную, монад-сопрограмму или операционную, либо вы можете использовать одну из многих потоковых абстракций, таких как канал, перечислитель или каналы, причем последняя является моей личной рекомендацией для простых случаев, подобных этой.

Каждый говорит вам использовать итерации, каналы или тому подобное, которые в настоящее время популярны. Но есть еще один классический способ сделать это! Просто используйте unsafeInterleaveIO от System.IO.Unsafe, Вся эта функция типа IO a -> IO a действительно, изменяет действие ввода-вывода так, чтобы оно фактически выполняло ввод-вывод только тогда, когда требуется значение thunk, а это именно то, о чем вы просили. Вы можете использовать это, чтобы написать iterateM с вашей желаемой семантикой тривиально.

Примеры как это где unsafeInterleaveIO светит.

Однако, я уверен, вы отметили "небезопасный" в названии - есть и другие примеры, где вы хотите получить прямой контроль над файловыми дескрипторами и использованием ресурсов или тому подобное, где unsafeInterleaveIO действительно будет плохой новостью, и потенциально может даже привести к нарушению ссылочной прозрачности.

(см. этот ответ для дальнейшего обсуждения: когда unsafeInterleaveIO небезопасен?)

Но опять же, в таком случае, я думаю, unsafeInterleaveIO это очевидный, правильный и простой результат.

Другие вопросы по тегам