3
\$\begingroup\$

First off, I'm just learning Haskell and I'm not proficient in that language yet.

I wrote this Haskell module to look for files in a Linux filesystem. It can use regular expressions as search patterns. I wrote two variants: one of them sends the search results to stdout, the other (shown here) gives a list with the search results.

For example (in GHCi): searchToList "conf" "." >>= return . take 1 (being "." my home directory) would give [".config"]

I made the function searchWriterT recursive using mapM_, but I know there is mapWriterT in the module Control.Monad.Writer.

Could anyone give me a hint about what I shall do to replace mapM_ with mapWriterT (if it's possible)?

Thanks in advance.

module FileSearch where

import System.Directory (getDirectoryContents, doesDirectoryExist)
import Control.Monad (filterM, unless)
import Data.List ((\\))
import Text.Regex.Posix ((=~))
import Control.Monad.Writer

data Dir = Dir { path :: FilePath -- path of the current directory
               , files :: [FilePath] -- files in the directory
               , subdirs :: [FilePath] } -- subdirectories
               deriving (Show)

dontSearch :: [FilePath]
dontSearch = ["/proc", "/sys"]

-- listDir takes the name of a directory and gives a Dir with
-- that name, a list of the files in the directory and a list of
-- its subdirectories, all prefixed with the path of the current
-- directory.        
-- listDir will list neither /proc nor /sys
listDir :: FilePath -> IO Dir
listDir f
    | f `elem` dontSearch = return Dir { path = f
                                       , files = []
                                       , subdirs = [] }
    | otherwise = do
        c' <- getDirectoryContents f
        let c = map (appendPath f) (deleteDot c')
        d <- filterM doesDirectoryExist c
        return Dir { path = f
                   , files = (c \\ d)
                   , subdirs = d }

appendPath :: FilePath -> FilePath -> FilePath
-- if the path is "/", do not add another "/"
appendPath "/" y = concat ["/", y]
appendPath x y = concat [x, "/", y]

-- remove "." and ".." entries in directories
deleteDot :: [FilePath] -> [FilePath]
deleteDot = filter (`notElem` [".", ".."])

searchWriterT :: String -> FilePath -> WriterT [FilePath] IO ()
searchWriterT s f = do
    d <- liftIO $ listDir f
    let found = filter (=~ s) (files d)
    tell found
    unless (null (subdirs d)) $
        mapM_ (searchWriterT s) (subdirs d)

searchToList :: String -> FilePath -> IO [FilePath]
searchToList s f = execWriterT (searchWriterT s f)
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

mapWriterT is for transforming one WriterT into another. Its type is painfully generic, but allows you to transform pretty much everything about a WriterT.

mapWriterT :: (m (a, w) -> n (b, w')) -> WriterT w m a -> WriterT w' n b

So if you start with a WriterT w m a, and you want to end up with WriterT w' n b (in other words, you can transform all 3 type parameters), then you have to provide a function of the form
m (a, w) -> n (b, w').


Contrast this with mapM_ :: (a -> m b) -> [a] -> m (). The purpose of this mapping is to take a list and perform monadic operations parameterized by the list's contents in sequential order.


While there may be a way to write your function in terms of mapWriterT, it is a completely different kind of mapping than mapM_, so your request to " replace mapM_ with mapWriterT" doesn't really make much sense to me. Nevertheless, let's give it a spin.

newtype MatchedFiles = MatchedFiles { unwrapFiles :: [FilePath] } deriving Monoid
newtype SubDirs = SubDirs { unwrapDirs :: [FilePath] }

filesInDir :: String -> FilePath -> WriterT MatchedFiles IO SubDirs
filesInDir s f = do
  d <- liftIO $ listDir f
  let found = MatchedFiles $ filter (=~ s) (files d)
      subdirs = SubDirs $ subdirs d
  tell found
  return subdirs

OK, so here we've got a non-recursive WriterT that will simply tell the files that match the string at the given directory, and returns the subdirectories in that directory. I've used newtypes to help me not get the "found files" and "subdirectories" mixed up.

Now, what is the transformation we want to perform on this?

searchWriterT :: String -> FilePath -> WriterT MatchedFiles IO ()
searchWriterT s f = mapWriterT mapper (filesInDir s f)
  where
    mapper :: IO (SubDirs, MatchedFiles) -> IO ((), MatchedFiles)
    mapper action = undefined

Assuming we want to write searchWriterT in terms of mapWriterT and filesInDir, I just followed the types and this is what we've got. Now, the question is, how to write mapper?

mapper action = do
  (SubDirs subdirs, files) <- action
  if null subdirs
    then return ((), files)
    else undefined

Clearly, if there are no "subdirs", then we are done, and we simply regurgitate the files. But what if there are subdirs? Well, you've already figured out what to do: use mapM_ on the subdirs!

else runWriterT $ tell files >> mapM_ (searchWriterT s) subdirs

In fact, mapM_ will take care of the null case for us: it won't do anything in the event of an empty list.

mapper action = do
  (SubDirs subdirs, files) <- action
  runWriterT $ tell files >> mapM_ (searchWriterT s) subdirs

Conclusion: mapWriterT felt kind of awkward for this problem, especially since inside the mapper we turned right around and used runWriterT. We didn't even get rid of mapM_, because mapM_ embodies exactly what needs to be done for this problem.

searchWriterT s f = filesInDir s f >>= mapM_ (searchWriterT s) . unwrapDirs
\$\endgroup\$
1
  • \$\begingroup\$ Thank you so much for your detailed answer! I (wrongly) thought the "map" in "mapWriterT" meant that the function could be used to iterate over a list, although this is not visible when looking at the signature. I'm giving your alternative a try, it might even perform better than mine. With your answer I also discovered that I was checking lists for emptiness, but this is redundant when using "map". \$\endgroup\$ Jun 15, 2012 at 5:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.