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?
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.