What is the idiomatic Haskell-way to act on predicates in IO?

Viewed 149

For some file operation, I need to check if the file exists, if it has been modified, and only then perform some operation on it. My newbie Haskell code looks as follows (simplified):

someFileOp ::FileContents -> FilePath -> IO (FileOpResult)
someFileOp contents absFilePath = do
    fileExists <- DIR.doesFileExist absFilePath
    if fileExists
        then do
            isMod <- isModified contents absFilePath
            if isMod
                then return FileModified
            else return $ doSomethingWithFile
        else return FileNotFound

It does work. However, the nested if-expressions look wrong to me - not FP-like. What would be an idiomatic way to check several Boolean conditions in IO and then take some action depending on their result?

3 Answers

Ignoring Daniel's good point on races and why checking for files is often just not done, the more Haskell solution is usually a monad transformer. This is a typical case where an ExceptT transformer makes sense. I've also included a (mis)use of ContT in case you're curious and want to explore:

import System.Directory as DIR
import Control.Monad.Trans.Cont
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Trans.Except

isModified :: a -> b -> IO Bool
isModified _ _ = pure False

type FileOpResult = Either String String

someFileOp_cont :: String -> FilePath -> IO FileOpResult
someFileOp_cont contents absFilePath = evalContT $ callCC $ \exit -> do
    fileExists <- liftIO $ DIR.doesFileExist absFilePath
    unless fileExists (exit (Left "FileNotFound"))
    isMod <- liftIO $ isModified contents absFilePath
    when isMod (exit (Left "FileModified"))
    return (Right "doSomethingWithFile")

someFileOp_except :: String -> FilePath -> IO FileOpResult
someFileOp_except contents absFilePath = runExceptT $ do
    fileExists <- liftIO $ DIR.doesFileExist absFilePath
    unless fileExists (throwE "FileNotFound")
    isMod <- liftIO $ isModified contents absFilePath
    when isMod (throwE "FileModified")
    return "doSomethingWithFile"

I'd use whenM :: Monad m => m Bool -> m () -> m() or ifM :: Monad m => m Bool -> m a -> m a -> m a, available for example in extra:

-- | Like 'when', but where the test can be monadic.
whenM :: Monad m => m Bool -> m () -> m ()
whenM mb mt = mb >>= \b ->
  if b
    then mt
    else return ()

-- | Like @if@, but where the test can be monadic.
ifM :: Monad m => m Bool -> m a -> m a -> m a
ifM mb mt me = mb >>= \b ->
  if b
    then mt
    else me

The code you posted looks fine to me. Another possibility would be to act in a short-circuiting monad like ExceptT Err IO.

data Err = FileNotFound | FileModified

getFileContents :: FilePath -> ExceptT Err IO FileContents
getFileContents fp = do
    exists <- doesFileExist fp
    if exists then {- ... -} else throwError FileNotFound

someFileOp :: FileContents -> FilePath -> ExceptT Err IO FileOpResult
someFileOp fc fp = do
    fc' <- getFileContents fp
    when (fc /= fc') (throwError FileModified)
    doSomethingWithFile
Related