I recently found myself needing to list all the subdirectories in the directory specified by a certain pathname.
There is getDirectoryContents in System.Directory.
getDirectoryContents :: FilePath -> IO [FilePath]
However, this will throw an Exception if the pathname is invalid. There is doesDirectoryExist, but using it in conjunction with getDirectoryContents would be non-atomic.
So the solution, it seems is either to use an external library, or one of the try functions, such as tryIOError.
This seems like an odd choice though, especially for a functional language. Why would these throw Exceptions when they could just return IO (Either e a)? I expected exceptions in Haskell to be something like panic! in Rust, where we use panic!() only for unrecoverable errors, and Result for everything else.
What's the idiomatic Haskell way of handling this kind of design choice? Should I be using try, or should I take exceptions as unrecoverable?
The design choice for System.Directory was probably based on the fact that, for example, System.IO already worked this way. The original design choice for System.IO might have had something to do with the fact that working within a nested monad like IO (Either e a) is tedious.
For example, consider a simple function based on the current design, like:
plainFiles :: FilePath -> IO [FilePath]
plainFiles dir = filterM (fmap isRegularFile . getFileStatus) =<< listDirectory dir
and now imagine that the signatures of the System.Directory functions are:
listDirectory' :: FilePath -> IO (Either IOError [FilePath])
getFileStatus' :: FilePath -> IO (Either IOError FileStatus)
Writing a straightforward implementation of plainFiles' :: FilePath -> IO (Either IOError [FilePath] is messy business.
An mtl-based solution might make sense, so functions would have signatures like:
listDirectory :: (MonadIO m, MonadError IOError m) => FilePath -> m [FilePath]
but not everyone wants to be forced to use mtl just because they want to list directory contents in their Haskell scriopt.
Anyway, it's easy enough to write a general purpose adapter for mtl.
liftEIO :: (MonadIO m, MonadError IOError m) => IO a -> m a
liftEIO act = liftIO (try act) >>= liftEither
which lets you adapt the functions you want to use:
listDirectory' :: (MonadIO m, MonadError IOError m) => FilePath -> m [FilePath]
listDirectory' = liftEIO . listDirectory
getFileStatus' :: (MonadIO m, MonadError IOError m) => FilePath -> m FileStatus
getFileStatus' = liftEIO . getFileStatus
and write programs in the monad of your choice:
type M = ExceptT IOError IO
plainFiles' :: FilePath -> M [String]
plainFiles' dir = filterM (fmap isRegularFile . getFileStatus') =<< listDirectory' dir
A larger, self-contained example:
import System.Environment
import Control.Exception
import Control.Monad.Except
import System.Directory
import System.Posix.Files
liftEIO :: (MonadIO m, MonadError IOError m) => IO a -> m a
liftEIO act = liftIO (try act) >>= liftEither
listDirectory' :: (MonadIO m, MonadError IOError m) => FilePath -> m [FilePath]
listDirectory' = liftEIO . listDirectory
getFileStatus' :: (MonadIO m, MonadError IOError m) => FilePath -> m FileStatus
getFileStatus' = liftEIO . getFileStatus
type M = ExceptT IOError IO
runM :: M a -> IO a
runM act = runExceptT act >>= liftEither
plainFiles' :: FilePath -> M [String]
plainFiles' dir = filterM (fmap isRegularFile . getFileStatus') =<< listDirectory' dir
main :: IO ()
main = runM $ do
[dir] <- liftIO getArgs
fs <- plainFiles' dir
liftIO . putStr $ unlines fs
If you have a plan for recovering from this problem, by all means use try or one of the other exception-handling mechanisms. That is exactly what they're there for.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With