It seems to me that exceptions in Haskell can be caught only immediately after they're thrown and are not propagated as in Java or Python. A short example illustrating this is below:
{-# LANGUAGE DeriveDataTypeable #-}
import System.IO
import Control.Monad
import Control.Exception
import Data.Typeable
data MyException = NoParseException String deriving (Show, Typeable)
instance Exception MyException
-- Prompt consists of two functions:
-- The first converts an output paramter to String being printed to the screen.
-- The second parses user's input.
data Prompt o i = Prompt (o -> String) (String -> i)
-- runPrompt accepts a Prompt and an output parameter. It converts the latter
-- to an output string using the first function passed in Prompt, then runs
-- getline and returns user's input parsed with the second function passed
-- in Prompt.
runPrompt :: Prompt o i -> o -> IO i
runPrompt (Prompt ofun ifun) o = do
putStr (ofun o)
hFlush stdout
liftM ifun getLine
myPrompt = Prompt (const "> ") (\s -> if s == ""
then throw $ NoParseException s
else s)
handleEx :: MyException -> IO String
handleEx (NoParseException s) = return ("Illegal string: " ++ s)
main = catch (runPrompt myPrompt ()) handleEx >>= putStrLn
After running the program, when you just press [Enter] whithout typing anything, I supposed to see: Illegal string:
in the output. Instead there appears: prog: NoParseException ""
. Suppose now that Prompt
type and runPrompt
function are defined in common library outside the module and cannot be changed to handle the exception in functions passed to Prompt constructor. How can I handle the exception without changing the runPrompt
?
I thought about adding the third field to Prompt
to inject exception-handling function this way, but it seems ugly to me. Is there a better choice?
The problem you're having is because you're throwing your exception in pure code: the type of throw
is Exception e => e -> a
. Exceptions in pure code are imprecise, and do not guarantee ordering with respect to IO
operations. So the catch
doesn't see the pure throw
. To fix that, you can use evaluate :: a -> IO a
, which "can be used to order evaluation with respect to other IO operations" (from the docs). evaluate
is like return, but it forces evaluation at the same time. Thus, you can replace liftM ifun getLine
with evaluate . ifun =<< getline
, which forces ifun
to have been evaluated during runPrompt
IO
action. (Recall that liftM f mx = return . f =<< mx
, so this is the same but with more control over evaluation.) And without changing anything else, you'll get the right answer:
*Main> :main
>
Illegal string:
Really, though, this isn't where I'd use exceptions. People don't use exceptions all that much in Haskell code, and particularly not in pure code. I'd much rather write Prompt
so that the input function's potential failure would be encoded in the type:
data Prompt o i = Prompt (o -> String) (String -> Either MyException i)
Then, running the prompt would just return an Either
:
runPrompt :: Prompt o i -> o -> IO (Either MyException i)
runPrompt (Prompt ofun ifun) o = do putStr $ ofun o
hFlush stdout
ifun `liftM` getLine
We'd tweak myPrompt
to use Left
and Right
instead of throw
:
myPrompt :: Prompt a String
myPrompt = Prompt (const "> ") $ \s ->
if null s
then Left $ NoParseException s
else Right s
And then we use either :: (a -> c) -> (b -> c) -> Either a b -> c
to handle the exception.
handleEx :: MyException -> IO String
handleEx (NoParseException s) = return $ "Illegal string: " ++ s
main :: IO ()
main = putStrLn =<< either handleEx return =<< runPrompt myPrompt ()
(Additional, unrelated, note: you'll notice I made some stylistic changes here. The only one I'd say is truly important is to use null s
, not s == ""
.)
If you really want the old behavior back at the top level, you can write runPromptException :: Prompt o i -> o -> IO i
which throws the Left
case as an exception:
runPromptException :: Prompt o i -> o -> IO i
runPromptException p o = either throwIO return =<< runPrompt p o
We don't need to use evaluate
here because we're using throwIO
, which is for throwing precise exceptions inside IO
computations. With this, your old main
function will work fine.
If you look at the type of myPrompt
, you’ll see that it’s Prompt o String
, i.e. not in IO
. For the smallest fix:
{-# LANGUAGE DeriveDataTypeable #-}
import System.IO
import Control.Monad
import Control.Exception
import Data.Typeable
data MyException = NoParseException String deriving (Show, Typeable)
instance Exception MyException
-- Prompt consists of two functions:
-- The first converts an output paramter to String being printed to the screen.
-- The second parses user's input.
data Prompt o i = Prompt (o -> String) (String -> IO i)
-- runPrompt accepts a Prompt and an output parameter. It converts the latter
-- to an output string using the first function passed in Prompt, then runs
-- getline and returns user's input parsed with the second function passed
-- in Prompt.
runPrompt :: Prompt o i -> o -> IO i
runPrompt (Prompt ofun ifun) o = do
putStr (ofun o)
hFlush stdout
getLine >>= ifun
myPrompt :: Prompt o String
myPrompt = Prompt (const "> ") (\s -> if s == ""
then throw $ NoParseException s
else return s)
handleEx :: MyException -> IO String
handleEx (NoParseException s) = return ("Illegal string: " ++ s)
main = catch (runPrompt myPrompt ()) handleEx >>= putStrLn
Though it might be more appropriate it to be Prompt o i e = Prompt (o -> String) (String -> Either i e)
.
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