5

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.

nicolas
  • 9,549
  • 3
  • 39
  • 83
  • 6
    Use `Maybe`. Catching errors is monadic—you can't *not* "force [it] to be monadic." It is monadic via its properties. – AJF Jan 03 '19 at 19:02
  • 11
    Write your own `data Ok a = No | Yes a`, don't write a `Monad` instance for it, and write a `safeHead :: [a] -> Ok a`. Then realize that you actually want it to be a `Monad` because being a `Monad` is really useful. Then use `Maybe` instead. – Rein Henrichs Jan 03 '19 at 19:07
  • 1
    Not sure to understand clearly your question. Can you be more precise, with an example of what you want to avoid? If it is only for `head`, you can use `Data.Maybe.listToMaybe`. – cdelmas Jan 04 '19 at 10:03
  • That would be specialized to Maybe, would require me to rewrite code (which in may case comes from SML), and any other exception could still be thrown. I want to make sure that a value of type a is indeed effect free by handling exception. – nicolas Jan 05 '19 at 10:42

3 Answers3

7

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.

Rein Henrichs
  • 15,437
  • 1
  • 45
  • 55
  • Instead of a custom `listElim` it may be worth suggesting `foldr`: `headDef = foldr const`. – amalloy Jan 03 '19 at 19:46
  • I mean I could have just written it with pattern matching, but this was more fun. – Rein Henrichs Jan 03 '19 at 19:49
  • The problem is : exception are handled in `IO` but generated in pure code. – nicolas Jan 05 '19 at 10:57
  • The usage of monadic was, maybe, not clear enough : I do not want to sequence my code in a particular way. Not all code using a maybe is structured as a sequence of steps propagating an optional value by wrapping/unwrapping, as CPS/monad does – nicolas Jan 05 '19 at 11:00
3

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.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • That's all right, my (imported) code is unsafe to begin with, and I am fine with that. Hopefully me handling errors won't make it *more* unsafe but less so :)) – nicolas Jan 05 '19 at 11:03
3

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
Anders Kaseorg
  • 3,657
  • 22
  • 35
  • 1
    `spoon` uses a combination of `unsafePerformIO` and `deepseq` that I would definitely not recommend for serious use. If you want a `head` that returns a `Maybe`, you can use the one in the 'safe' package. – Rein Henrichs Jan 04 '19 at 02:55
  • 1
    @ReinHenrichs I wouldn’t _recommend_ it either, but the OP did say “which I do not want to make pure for real, nor force to be monadic”, and at least `spoon` (or `teaspoon`, if you don’t want the `deepseq`) is safer than writing `unsafePerformIO` directly. – Anders Kaseorg Jan 04 '19 at 03:22
  • How is returning a Maybe via unsafe magic any less "monadic" than just returning a Maybe the usual way? If they're ok with Maybe, why not just suggest headMay? – Rein Henrichs Jan 04 '19 at 04:20
  • In fact, isn't it *more* monadic since it uses IO under the hood? – Rein Henrichs Jan 04 '19 at 04:21
  • 2
    Then again, I confess that I often don't know what people mean when they say "monadic" (or, for that matter, "pure"). – Rein Henrichs Jan 04 '19 at 04:25
  • 3
    @ReinHenrichs I’m imagining that the OP has some kind of giant complicated computation with a `head` deep buried inside layers and layers of functions that were all built with the assumption that the `head` would never receive an empty list—an assumption that for some reason has now changed. The hard part of fixing this the right way isn’t getting a `Maybe` back from `head`—that’s easy—the hard part is tediously fixing all the layers of functions to plumb the `Maybe` all the way back to the outer caller monadically. And so, I imagine, the OP is looking for a quick and dirty shortcut. – Anders Kaseorg Jan 04 '19 at 04:37
  • 2
    Obviously, this is a terrible software engineering decision and it’s probably going to come back to bite them. But some one-off projects don’t require good software engineering. Or at least that’s what I tell myself when I’m trying not to be cynical about the entire field of computer science… – Anders Kaseorg Jan 04 '19 at 04:45
  • That is... a bit of a stretch? – Rein Henrichs Jan 04 '19 at 04:46
  • @ReinHenrichs Your recommendation compares safe code VS unsafe code. the problem here is "how to deal with unsafe code", and we should not be fooled by Haskell's pure type which are in reality not pure at all (`head` being just one example) – nicolas Jan 05 '19 at 10:51
  • @AndersKaseorg That's exactly the situation. It's good that haskell is promoting purity, but it's also true that we need practical ways to deal with haskell lies about it. The imported code is throwing exception somewhere, I know why, and I have no desire to rewrite the whole damn thing just to make it work, when this could be reached in one step. – nicolas Jan 05 '19 at 10:56
  • @ReinHenrichs monadic in that context meant giving an explicit sequencing of operations in the syntax, which is sometimes necessary, and often not. I can imagine it's an overloaded term, I should have made it more clear. – nicolas Jan 05 '19 at 11:06
  • @nicolas It definitely wasn't clear to me that your question wanted this sort of answer, but I'm glad you got some good answers. – Rein Henrichs Jan 05 '19 at 17:03