Unit-testing the undefined evaluated in lazy expre

2019-07-20 06:21发布

问题:

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

回答1:

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.