Because I oversimplified in my other question before, I would like to give a more clear example here.
How can I handle situations where I have to check for certian conditions in a sequential way without nesting multiple cases? With "sequential way" I mean getting a value (e.g. from stdin), checking this value for a certain condition and depending on the outcome getting another value and so on.
Example:
sequen :: IO String
sequen = do
a <- getLine
case a of
"hi" -> do
putStrLn "hello!"
b <- getLine
case b of
"how are you?" -> do
putStrLn "fine, thanks"
return "nice conversation"
_ -> return "error 2"
_ -> return "error 1"
I know that there are better ways to write such a chat bot, it should just demonstrate the sequential nature of the problem. As you can see, with every nested case, the code also gets indented deeper.
Is there a way to better structure such code? I'm thinking of handling the "errors" on one place and describing the "success-path" without the error handling distributed all over it.
Of course. This is precisely what EitherT
was made for. You can get it from Control.Monad.Trans.Either
in the eitherT
package.
import Control.Monad.Trans.Class
import Control.Monad.Trans.Either
main = do
e <- runEitherT $ do
a <- lift getLine
case a of
"hi" -> lift $ putStrLn "hello!"
_ -> left 1
b <- lift getLine
case b of
"how are you?" -> lift $ putStrLn "fine, thanks!"
_ -> left 2
return "nice conversation"
case e of
Left n -> putStrLn $ "Error - Code: " ++ show n
Right str -> putStrLn $ "Success - String: " ++ str
EitherT
aborts the current code block whenever it encounters a left
statement, and people typically use this to indicate error conditions.
The inner block's type is EitherT Int IO String
. When you runEitherT
it, you get IO (Either Int String)
. The Left
type corresponds to the case where it failed with a left
and the Right
value means it successfully reached the end of the block.
I wrote a series of posts a while back going over my own learnings of the Either
& EitherT
types. You can read it here: http://watchchrislearn.com/blog/2013/12/01/working-entirely-in-eithert/
I use the errors
package to get a bunch of nice helpers around using EitherT (left
and right
functions for instance to return lifted versions of Left
and Right
).
By extracting your potential failure conditions into their own helpers, you can make the mainline of your code read totally sequentially, with no case statements checking results.
From that post, you can see how the runEitherT
section is a sequential chunk of work, it just happens to have the failure mechanics of EitherT
. Obviously this code is fairly contrived to show how MaybeT
plays inside of EitherT
as well. In real code it'd just be the story you were wanting to tell, with a single Left
/Right
at the end.
import Control.Error
import Control.Monad.Trans
-- A type for my example functions to pass or fail on.
data Flag = Pass | Error
main :: IO ()
main = do
putStrLn "Starting to do work:"
result <- runEitherT $ do
lift $ putStrLn "Give me the first input please:"
initialText <- lift getLine
x <- eitherFailure Error initialText
lift $ putStrLn "Give me the second input please:"
secondText <- lift getLine
y <- eitherFailure Pass (secondText ++ x)
noteT ("Failed the Maybe: " ++ y) $ maybeFailure Pass y
case result of
Left val -> putStrLn $ "Work Result: Failed\n " ++ val
Right val -> putStrLn $ "Work Result: Passed\n " ++ val
putStrLn "Ok, finished. Have a nice day"
eitherFailure :: Monad m => Flag -> String -> EitherT String m String
eitherFailure Pass val = right $ "-> Passed " ++ val
eitherFailure Error val = left $ "-> Failed " ++ val
maybeFailure :: Monad m => Flag -> String -> MaybeT m String
maybeFailure Pass val = just $ "-> Passed maybe " ++ val
maybeFailure Error _ = nothing
Since you are necessarily in the IO
monad, you are better off using the IO
monad's error handling capabilities instead of stacking an error monad on top of IO
. It avoids all of the heavy lift
ing:
import Control.Monad ( unless )
import Control.Exception ( catch )
import Prelude hiding ( catch )
import System.IO.Error ( ioeGetErrorString )
main' = do
a <- getLine
unless (a == "hi") $ fail "error 1"
putStrLn "hello!"
b <- getLine
unless (b == "how are you?") $ fail "error 2"
putStrLn "fine, thanks"
return "nice conversation"
main = catch main' $ return . ioeGetErrorString
In this case, your errors are simply String
s, which are thrown by IO
's fail
, as a userError
. If you want to throw some other type, you will need to use throwIO
instead of fail
.
At some point the EitherT
package was deprecated (though transformers-either
offers a similar API). Fortunately there's an alternative to EitherT
that doesn't even require installing a separate package.
The standard Haskell installation comes with the Control.Monad.Trans.Except
module (from the transformers
package, which is bundled with GHC), which behaves almost identically to EitherT
. The resulting code is almost identical to the code in Gabriella Gonzalez's answer, but using runExceptT
instead of runEitherT
and throwE
instead of left
.
import Control.Monad.Trans.Class
import Control.Monad.Trans.Except
main = do
e <- runExceptT $ do
a <- lift getLine
case a of
"hi" -> lift $ putStrLn "hello!"
_ -> throwE 1
b <- lift getLine
case b of
"how are you?" -> lift $ putStrLn "fine, thanks!"
_ -> throwE 2
return "nice conversation"
case e of
Left n -> putStrLn $ "Error - Code: " ++ show n
Right str -> putStrLn $ "Success - String: " ++ str
(Note that the aforementioned transformers-either
package is in fact a wrapper for ExceptT
designed for providing compatibility with code that still uses EitherT
.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With