As you noticed, this function is slow because it collects all the results before it starts printing them. To circumvent that problem, you need to interleave the collection of information and its printing.
A good way of doing that whilst keeping a compositional approach to solving the problem is to introduce a datatype reifying the structure of walkDir
's call graph. Instead of sequencing all IO
actions and getting a list of FilePath
s back, you'd build a tree describing the computation (RTree
for Rose Tree and T
for Transformer as it takes an m
):
data RTreeT m a = Node a [m (RTreeT m a)]
You can now write walkDir'
describing your strategy to explore the directories on your filesystem: return the files present in the current directory immediately and then explore the subdirectories one after the other.
walkDir' :: FilePath -> IO (RTreeT IO [FilePath])
walkDir' r = do
contents <- fmap (r </>) . exceptLocal <$> getDirectoryContents r
(files, dirs) <- filesAndDirs contents
return $ Node files $ fmap walkDir' dirs
where filesAndDirs
partitions a list of FilePath
depending on whether they are files or directories (using tagDirectories
to perform that test).
tagDirectories :: [FilePath] -> IO [(FilePath, Bool)]
tagDirectories = mapM (\ x -> (x,) <$> doesDirectoryExist x)
filesAndDirs :: [FilePath] -> IO ([FilePath], [FilePath])
filesAndDirs c = bimap (fmap fst) . partition (not . snd) <$> tagDirectories c
where bimap f (a, b) = (f a, f b)
and exceptLocal
is the filter you had in your original code snippet:
exceptLocal :: [FilePath] -> [FilePath]
exceptLocal = filter ((&&) . (/=) "." <*> (/=) "..")
You now have an RTreeT IO [FilePath]
and you can described a strategy to print it which will interleave printing some of the content and running some of the remaining IO
actions:
printRTreeT :: Show a => RTreeT IO a -> IO ()
printRTreeT (Node a mts) = print a >> mapM_ (printRTreeT =<<) mts
Of course, this is a rather crude printing function (e.g. you will notice quite a few empty lists if you have empty subdirectories) but it gives you an idea of how to proceed from there on.
If this is still slow, you may want to play the same sort of trick on filesAndDirs
: rather than sequencing all tests in one go, you could want to have a structure allowing you to only deal with one FilePath
at a time.
if
with a bind directly, you could just do\e -> if e then walkDir x else return [x]
. That's what thedo
notation desugars to anyway. \$\endgroup\$ifFunction x y z = if x then y else z
. \$\endgroup\$