Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trying to understand error-catching in Haskell

Tags:

haskell

While trying to write a program in Haskell, I suddenly realized that I apparently don't understand how error throwing/catching excetions works. While my actual case is significantly more complicated, I've come up with a seemingly minimal example displaying what I don't understand:

import Control.Exception
import Control.Monad
import Data.Typeable

data IsFalse = IsFalse
    deriving (Show, Typeable)

instance Exception IsFalse

isTrue :: Bool -> Bool
isTrue b = if b then b else throw IsFalse

catchesFalse :: Bool -> IO ()
catchesFalse = try . return . isTrue >=> either (\e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")

main :: IO ()
main = catchesFalse False

When running with runhaskell, I would expect the above code to fail and print IsFalse. However, it instead prints uh-oh. On the other hand, if I replace the definition of catchesFalse by

catchesFalse = try . return . isTrue >=> either (\e -> fail $ displayException (e :: IsFalse)) print

then the exception is caught, just as I would expect.

I'm hoping that someone can point me to any resources that could help me understand the discrepency between these two functions. My best guess is that there's something going on with lazy evaluation, but I'm not sure.

If this is indeed the case, what's the best method to force Haskell to evaluate an expression to the point where it could catch an exception? Forgive me, I understand that this particular question likely has many answers depending on what I actually care to evaluate (which, in my actual case, isn't anywhere near as simple as Bool).

like image 392
munchhausen Avatar asked Oct 14 '20 04:10

munchhausen


People also ask

Is either a Monad Haskell?

Today I'll start with a simple observation: the Either type is a monad! For a long time, I used Either as if it were just a normal type with no special rules. But its monadic behavior allows us to chain together several computations with it with ease!

What does Just do in Haskell?

It represents "computations that could fail to return a value". Just like with the fmap example, this lets you do a whole bunch of computations without having to explicitly check for errors after each step.


Video Answer


1 Answers

What you probably want is evaluate:

catchesFalse = try . evaluate . isTrue >=> either (\e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")

With this definition, catchesFalse False will result in

*** Exception: user error (IsFalse)

Note that the user error here is a hint that this has actually been produced by fail.

Both your examples don't "catch" the exception. The second one triggers it by means of calling print.

Exceptions in "pure" (i.e., non-IO) computations are tricky. In fact, we have the following equalities

  try (return e) >>= f
=
  return (Right e) >>= f
=
  f (Right e)

Let's look at the first equation, which is probably the more surprising. The function try is implemented in terms of catch, and catch wraps the given IO computation and checks whether in its execution there are any effects. However, execution does not mean evaluation, and it only concerns the "effectful" part of the computation. A return is a trivial IO computation that "succeeds" immediately. Neither catch nor try are going to act on this, regardless of what the result looks like.

The second equation simply follows from the monad laws.

If we keep this in mind, and apply equational reasoning to your examples, we get in the first case:

  catchesFalse False
=
  (try . return . isTrue >=> either (\ e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")) False
=
  try (return (isTrue False)) >>= either (\ e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")
=
  return (Right (isTrue False)) >>= either (\ e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh")
=
  either (\ e -> fail $ displayException (e :: IsFalse)) (const $ putStrLn "uh-oh") (Right (isTrue False))
=
  (const $ putStrLn "uh-oh") (isTrue False)
=
  putStrLn "uh-oh"

So as you can see, the exception is never even triggered.

In the second example, everything is the same until almost the end, and we get

  either (\ e -> fail $ displayException (e :: IsFalse)) print (Right (isTrue False))
=
  print (isTrue False)

Now, when executing this, print will force its argument, and thereby trigger the exception, and this will yield the output:

*** Exception: IsFalse

This is coming directly from throw, not from your handler; there's not user error in the output.

The use of evaluate changes this in returning an IO action that forces its argument to weak head normal form before "returning", thereby lifting a certain amount of exceptions that arise during evaluation of the argument expression into exceptions that can be caught during execution of the resulting IO action.

Note, however, that evaluate does not fully evaluate its argument, but only to weak head normal form (i.e., the outermost constructor).

All in all, a lot of care is necessary here. In general, it is advisable to avoid exceptions in "pure" code, and to use types that explicitly allow failure (such as Maybe and variants) instead.

like image 167
kosmikus Avatar answered Oct 22 '22 18:10

kosmikus