在 Haskell 中对惰性表达式中评估的未定义进行单元测试

Unit-testing the undefined evaluated in lazy expression in Haskell

在 Haskell 中编写单元测试,其中遇到 undefined 时表达式应该失败,这有点棘手。我用 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

无济于事。它报告我 did not get expected exception: SomeException

如果我在 REPL 中计算相同的表达式,我会得到:

[*** 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

问题是 evaluate 不会强制您的表达式为 NH 甚至 WHNF1。在 GHCi 中尝试 x <- evaluate (take 1 $ map (+1) [undefined, 2, 3]) - 它不会给您任何错误!当您粘贴 evaluate (take 1 $ map (+1) [undefined, 2, 3]) 时,它这样做的唯一原因是 GHCi 还尝试打印它得到的结果,为此,它最终会尝试计算表达式。

如果你想知道有多少 thunk 被评估了,你总是可以在 GHCi 中使用 :sprint:

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

如您所见,evaluate 没有强制表达式足够远以实现 x 包含 undefined。一个快速的解决方法是使用 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 允许您触发 thunk 的评估,直到参数被完全评估。请注意,它有一个 NFData(代表 "normal form data")约束,因此您可能会发现自己为数据结构派生了 GenericNFData


1 感谢@AlexisKing 指出 evaluate 确实 将其论点推入 WNHF,这就是为什么 head $ map (+1) [undefined, 2, 3] 确实会触发错误。在 take 的情况下,这还不够。