Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Catching Exceptions in Haskell

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?

like image 203
Sventimir Avatar asked May 04 '14 15:05

Sventimir


2 Answers

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.

like image 74
Antal Spector-Zabusky Avatar answered Nov 14 '22 14:11

Antal Spector-Zabusky


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

like image 23
Ry- Avatar answered Nov 14 '22 13:11

Ry-