Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to simplify the error handling in (IO (Either a b))

Tags:

haskell

I'm using the following scenario as an example to learn how to handle errors in a simple way. The scenario is basically read a file path from an environment variable, then read and print the file with the file path.

The following code works, but I don't like the printFile because it has nested case of, a bit hard to read. I wonder if there is a clean way to get rid of it and keep the printFile function flat without using lookupEnv?

How would you simplify this error handling flow?

module Main where

import Control.Exception (IOException, handle, throw)
import System.Environment (getEnv)
import System.IO.Error (isDoesNotExistError)

data MissingError
  = MissingEnv String
  | MissingFile String
  deriving (Show)

main :: IO ()
main = do
  eitherFile <- printFile
  either print print eitherFile

getEnv' :: String -> MissingError -> IO (Either MissingError String)
getEnv' env err = handle (missingEnv err) $ Right <$> (getEnv env)

readFile' :: FilePath -> MissingError -> IO (Either MissingError String)
readFile' path err = handle (missingFile err) $ Right <$> (readFile path)

missingEnv :: MissingError -> IOException -> IO (Either MissingError String)
missingEnv err = const $ return $ Left err

missingFile :: MissingError -> IOException -> IO (Either MissingError String)
missingFile err e
  | isDoesNotExistError e = return $ Left err
  | otherwise = throw e

printFile :: IO (Either MissingError String)
printFile = do
  eitherFilePath <- getEnv' "FOLDER" (MissingEnv "FOLDER")
  case eitherFilePath of
    Left err -> return $ Left err
    Right path -> readFile' path (MissingFile path)
like image 981
Leo Zhang Avatar asked Nov 22 '18 08:11

Leo Zhang


2 Answers

You can use the ExceptT monad transformer for this. I haven't tried to run the following proposed changes, but it compiles, so I hope it works.

First, import the module that contains ExceptT:

import Control.Monad.Trans.Except

Next, change the printFile function:

printFile :: IO (Either MissingError String)
printFile = runExceptT $ do
  path <- ExceptT $ getEnv' "FOLDER" (MissingEnv "FOLDER")
  ExceptT $ readFile' path (MissingFile path)

You have functions that return IO (Either MissingError String), so wrapping them in ExceptT gives you do notation that enables you to access the String embedded in what's effectively ExcepT MissingError IO String.

Then unwrap the ExceptT return value with runExceptT.

like image 127
Mark Seemann Avatar answered Jan 03 '23 16:01

Mark Seemann


The suggestion to use ExceptT is of course a good one but IMHO the proposed answer is still somewhat verbose and you can go a bit farther by simply "staying" in the ExceptT monad throughout your code. Also I would not recommend handling IO exceptions all over the place. Even with a small code base you would lose oversight of your code quickly. tryIOError is useful in this regard. And finally rethinking the definition of your errors would also yield easier to understand and a more solid solution. The end result would look something like this:

module Main where

import Data.Bifunctor       (first)
import Control.Monad.Except (ExceptT(..), runExceptT)
import System.Environment   (getEnv)
import System.IO.Error      (tryIOError, isDoesNotExistError)

data MyError = MissingError String
             | SomeIOError IOError
               deriving (Show)

main :: IO ()
main = do
  result <- runExceptT printFile
  print result

getEnv' :: String -> ExceptT MyError IO String
getEnv' env = mapIOError ("getting env var " ++ env) $ getEnv env

readFile' :: FilePath -> ExceptT MyError IO String
readFile' path = mapIOError ("reading file " ++ path) $ readFile path

printFile :: ExceptT MyError IO String
printFile = do
  path <- getEnv' "FOLDER"
  readFile' path

mapIOError :: String -> IO a -> ExceptT MyError IO a
mapIOError msg = ExceptT . fmap (first mapError) . tryIOError
    where mapError err | isDoesNotExistError err = MissingError msg
          mapError err                           = SomeIOError err
like image 37
Erick Gonzalez Avatar answered Jan 03 '23 16:01

Erick Gonzalez