Pattern match exception timing in Haskell

Viewed 171

Consider this (rather cut-down) function for catching the failure of pattern matching.

module Handle where
import Control.Exception
import System.IO.Unsafe

wrap :: (a -> b) -> a -> b
wrap f x =
  unsafePerformIO (catch (return $! (f x)) handler)
  where
    handler :: PatternMatchFail -> b
    handler _ = error "caught"

I've put it in a separate module to stop it being inlined into the examples that follow. Here is an example of using the wrapper.

module Main where
import Handle

f [x] = x

g [x] y = x + y

h [x] = \ y -> x + y

main = do putStrLn (show (wrap h [] 4))

Let's try it with GHCi. As expected, in simple examples, wrap either returns the value of f x, or if the pattern fails, it traps the error:

mike@spinnaker:~$ ghci example.hs
GHCi, version 8.4.4: http://www.haskell.org/ghc/  :? for help
[1 of 2] Compiling Handle           ( Handle.hs, interpreted )
[2 of 2] Compiling Main             ( example.hs, interpreted )
Ok, two modules loaded.
*Main> wrap f [3]
3
*Main> wrap f []
*** Exception: caught

But now consider the functions g and h, which ought to be equivalent to each other. The wrap function doesn't work with g, leaving the failure to be caught at the top level, but it does work with h.

*Main> wrap g [3] 4
7
*Main> wrap g [] 4
*** Exception: example.hs:6:1-15: Non-exhaustive patterns in function g

*Main> wrap h [] 4
*** Exception: caught

Now let's try GHC itself. The results are that, with optimisation turned off, the error from h is caught; but under -O the optimiser presumably turns h into g, and the error is no longer caught.

mike@spinnaker:~$ ghc -O --make -o example example.hs
mike@spinnaker:~$ example
example: example.hs:8:1-20: Non-exhaustive patterns in function h

I can well imagine that the optimiser turns the nested function h into a two-arguments-at-once function like g, and that applying g to a single argument yields an object that is considered a head-normal form until it receives its second argument, so the pattern matching failure is delayed until we are outside the scope of wrap. My questions are these: is this explanation broadly correct? And if we want consistent behaviour, how can we achieve it?

(The example is extracted from an interpreter for a higher-order programming langugage, where the partial functions are installed as primitives, and we want a uniform method of dealing with failure of primitives, whether they deliver a higher-order result or not: that varies according to the underlying monad.)

2 Answers

Here's a workable scheme. In the example, the argument [x] represents the argument list of a primitive in the interpreter, and y is an artefact of the monad in use ( perhaps M a = Mem -> (a, Mem) ). [I am using monads, but not wrapping them in type classes. To each his own!]

First, a version of wrap that prints the function name and offending argument list. Note that the res parameter is the result that should be returned -- CBN means that there's no need to form the application f args inside wrap when it can be passed in from outside.

module Handle where
import Control.Exception
import System.IO.Unsafe

wrap :: Show a => String -> a -> b -> b
wrap f args res =
  unsafePerformIO (catch (return $! res) handler)
  where
    handler :: PatternMatchFail -> b
    handler _ = error ("caught " ++ f ++ " " ++ show args)

Now an example, with wrapping of both one-argument and two-argument functions. The helper function wrap2 has one definition for each version of the interpreter, whereas f, g and h are members of a big family of primitives.

module Main where
import Handle

f [x] = x
ff arg = wrap "f" arg (f arg)

wrap2 :: Show a => String -> (a -> b -> c) -> a -> b -> c
wrap2 name fun arg y = wrap name arg (fun arg y)

g [x] y = x + y
gg = wrap2 "g" g

h [x] = \ y -> x + y
hh = wrap2 "h" h

main = do putStrLn (show (hh [] 4))

The goal is to be able to define partial primitives like these with a one-line definition for each primitive.

The transformation of moving a lambda out of a case expression, and a related one that affects the IO monad, can be disabled by giving GHC the flags -fpedantic-bottoms and -fno-state-hack. That seems the best solution to this difficulty, rather than trying to guess what wrapper function is needed in each case.

Related