Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is catching Exceptions Considered Code-smell in Haskell? [closed]

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?

like image 799
doliphin Avatar asked Dec 02 '25 01:12

doliphin


2 Answers

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
like image 80
K. A. Buhr Avatar answered Dec 03 '25 23:12

K. A. Buhr


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.

like image 33
Daniel Wagner Avatar answered Dec 03 '25 21:12

Daniel Wagner



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!