在 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")约束,因此您可能会发现自己为数据结构派生了 Generic
和 NFData
。
1 感谢@AlexisKing 指出 evaluate
确实 将其论点推入 WNHF,这就是为什么 head $ map (+1) [undefined, 2, 3]
确实会触发错误。在 take
的情况下,这还不够。
在 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")约束,因此您可能会发现自己为数据结构派生了 Generic
和 NFData
。
1 感谢@AlexisKing 指出 evaluate
确实 将其论点推入 WNHF,这就是为什么 head $ map (+1) [undefined, 2, 3]
确实会触发错误。在 take
的情况下,这还不够。