2

Writing a unit test in Haskell where an expression should fail when undefined is encountered is a bit tricky. I tried the following with HSpec:

module Main where

import Test.Hspec
import Control.Exception (evaluate)

main :: IO ()
main = hspec $ do
  describe "Test" $ do
    it "test case" $ do
      evaluate (take 1 $ map (+1) [undefined, 2, 3]) `shouldThrow` anyException

to no avail. It reports me did not get expected exception: SomeException

If I evaluate the same expression in REPL, I'd get:

[*** Exception: Prelude.undefined
CallStack (from HasCallStack):
  error, called at libraries\base\GHC\Err.hs:79:14 in base:GHC.Err
  undefined, called at <interactive>:2:20 in interactive:Ghci1
Artem Oboturov
  • 4,344
  • 2
  • 30
  • 48

1 Answers1

4

The problem is that evaluate doesn't force your expression to NH or even WHNF1. Try x <- evaluate (take 1 $ map (+1) [undefined, 2, 3]) in GHCi - it doesn't give you any error! The only reason it does when you paste in evaluate (take 1 $ map (+1) [undefined, 2, 3]) is that GHCi also tries to print the result of what it got and, to do that, it ends up trying to evaluate the expression.

If you want to see how much of a thunk has been evaluated, you can always use :sprint in GHCi:

ghci> x <- evaluate (take 1 $ map (+1) [undefined, 2, 3])
ghci> :sprint x
x = [_]

As you can see, evaluate hasn't forced the expression far enough to realize x contains an undefined. A quick fix is to evaluate the thing you are examining to normal form using force.

import Test.Hspec
import Control.Exception (evaluate)
import Control.DeepSeq (force)

main :: IO ()
main = hspec $ do
  describe "Test" $ do
    it "test case" $ do
      evaluate (force (take 1 $ map (+1) [undefined, 2, 3] :: [Int])) 
        `shouldThrow` anyException

force lets you trigger the evaluation of thunks until the argument is full evaluated. Note that it has an NFData (stands for "normal form data") constraint on it, so you may find yourself deriving Generic and NFData for your data structures.


1 Thanks for @AlexisKing for pointing out that evaluate does push its argument into WNHF, which is why head $ map (+1) [undefined, 2, 3] does trigger the error. In the case of take, it isn't enough though.

Alec
  • 31,829
  • 7
  • 67
  • 114
  • This answer is good, but I’m confused when you say that `evaluate` doesn’t evaluate to WHNF when [the docs explicitly state that it evaluates its argument to WHNF](https://hackage.haskell.org/package/base-4.9.0.0/docs/Control-Exception.html#v:evaluate). – Alexis King Dec 14 '16 at 21:24
  • @AlexisKing I stand corrected! In any case, WNHF is not enough, hence why you need `NFData`. – Alec Dec 14 '16 at 21:29
  • So, actually, the test passes because it produces a list with a single undefined element? Why isn't the (+1) function applied? – Artem Oboturov Dec 15 '16 at 07:50
  • Since there is no need to inspect the first element of the list, Haskell doesn't bother to try to evaluate `(+1) undefined`. The thunk is there, but Haskell doesn't need to force it for `evaluate`. – Alec Dec 15 '16 at 08:06