How to catch error in pure haskell code with no monad

Viewed 716

It's probably been asked to death, but would anyone know the least intrusive way to "catch"

catch
:: Exception e   
=> IO a 
-> (e -> IO a)  
-> IO a

error in "pure" haskell computations ?

(say, some code containing head [], not safe, which I do not want to make pure for real, nor force to be monadic)

PS : of course if you can and want to design from scratch, do bake that in (with a maybe type). The question here is that I do knowingly want to keep it unsafe (to keep a simple translation from an unsafe language) and wonder what the nicest/least ugly way to do so.

3 Answers

There is no way to use catch in pure code. This is by design: exceptions are handled by the IO system. That's why the type of catch uses IO. If you want to handle failure in pure code, you should use a type to represent the possibility of failure. In this case, the failure is that the value can sometimes not exist.

The type we use in Haskell to denote a value that can either exist or not is called Maybe. Maybe is an instance of Monad, so it is "monadic", but that should not dissuade you from using it for its intended purpose. So the function you want is headMay from the safe package: headMay :: [a] -> Maybe a.

That said, if you want to avoid monads you can instead use a function that unpacks the list:

listElim :: b -> (a -> [a] -> b) -> [a] -> b
listElim nil _ [] = nil
listElim _ cons (x:xs) = cons x xs

As you can see, this replaces a [] with nil and a : with a call to cons. Now you can write a safe head that lets you specify a default:

headDef :: a -> [a] -> a
headDef def = listElim def const

Unfortunately, functions are an instance of Monad, so it turns out that you have been "force[d] to be monadic" after all! There is truly no escaping the monad, so perhaps it is better to learn how to use them productively instead.

If you want to live dangerously, you can use unsafePerformIO:

catch'Pure'
  :: Exception e   
  => a 
  -> (e -> a)
  -> a
catch'Pure' v h = unsafePerformIO $
  evaluate v `catch` (pure . h)

The problem is that this isn't guaranteed to be at all well-behaved. For example, if you pass it the value

(let a = a in a) `seq` error "hallo!"

the compiler is entitled to produce an infinite loop sometimes and an error message other times, violating the fundamental expectation of purity. There are reasons to use code that looks sort of like this, but great care is required to make it behave well.

Control.Spoon from the spoon package provides a mostly-safe wrapper around the unsafe operations required for this.

λ> spoon (head [1,2,3])
Just 1
λ> spoon (head [])
Nothing
Related