Search code examples
haskelltypesioio-monad

Haskell Safe IO


I am attempting to write a simple function to safely read a file (if it exists) and do nothing if the file does not exist:

safeRead :: String -> IO ()
safeRead path = readFile path `catch` handleExists
  where handleExists e
          | isDoesNotExistError e = return ()
          | otherwise = throwIO e

This fails at compile time with

Couldn't match type ‘[Char]’ with ‘()’
Expected type: IO ()
  Actual type: IO String
In the first argument of ‘catch’, namely ‘readFile path’
In the expression: readFile path `catch` handleExists

This makes sense since :t readFile is readFile :: FilePath -> IO String. e.g a function that returns IO String (and IO String is not the same as IO ())

Changing the signature to String -> IO String

Couldn't match type ‘()’ with ‘[Char]’
Expected type: IOError -> IO String
  Actual type: IOError -> IO ()
In the second argument of ‘catch’, namely ‘handleExists’
In the expression: readFile path `catch` handleExists

Which also makes sense since handleExists has type IO ()

To save everyone lookup, catch is imported with: import Control.Exception the signature of catch is: catch :: Exception e => IO a -> (e -> IO a) -> IO a

My real question is, how can I write this kind of error safe, flexible code in Haskell? More specifically what would be the change I would have to make to this function to have it handle both a success case and a failure case?


Solution

  • You need to figure out what you want your function to actually do.

    If it successfully reads the file, you want it to return the contents as a string.

    If it fails, what do you actually want it to do? Return an empty string or some other fallback content? Then you can just change the return () to return "" in the first case of handleExists.

    But if you want to indicate the error in the return type, then you need to return a different type than just String. As Carsten said, you can return a Maybe String and give Just theString for success and Nothing for error. Or you could return an Either if you want some error message instead.

    I feel that for this particular function, Maybe String makes the most sense, since you only catch non-presence of the file and rethrow other errors. Then your code needs to look like this:

    safeRead :: String -> IO (Maybe String)
    safeRead path = (fmap Just $ readFile path) `catch` handleExists
      where
        handleExists :: IOException -> IO (Maybe String)
        handleExists e
          | isDoesNotExistError e = return Nothing
          | otherwise = throwIO e
    

    Here we wrap the result of readFile in a Just to fulfill the type requirement, and in the error case return Nothing instead of unit.