【问题标题】:Interaction between optimizations and testing for error calls优化和错误调用测试之间的交互
【发布时间】:2011-04-17 23:23:49
【问题描述】:

我在一个模块中有一个看起来像这样的函数:

module MyLibrary (throwIfNegative) where

throwIfNegative :: Integral i => i -> String
throwIfNegative n | n < 0 = error "negative"
                  | otherwise = "no worries"

我当然可以返回Maybe String 或其他一些变体,但我认为使用负数调用此函数是程序员的错误,因此在这里使用error 是合理的。

现在,因为我喜欢 100% 的测试覆盖率,所以我想要一个测试用例来检查这种行为。这个我试过了

import Control.Exception
import Test.HUnit

import MyLibrary

case_negative =
    handleJust errorCalls (const $ return ()) $ do
        evaluate $ throwIfNegative (-1)
        assertFailure "must throw when given a negative number"
  where errorCalls (ErrorCall _) = Just ()

main = runTestTT $ TestCase case_negative

它有点工作,但在使用优化编译时失败:

$ ghc --make -O Test.hs
$ ./Test
### Failure:                              
must throw when given a negative number
Cases: 1  Tried: 1  Errors: 0  Failures: 1

我不确定这里发生了什么。似乎尽管我使用了evaluate,但该函数并未得到评估。此外,如果我执行以下任何步骤,它会再次起作用:

  • 去掉HUnit,直接调用代码
  • throwIfNegative 移动到与测试用例相同的模块中
  • 删除throwIfNegative的类型签名

我认为这是因为它会导致应用不同的优化。有什么指点吗?

【问题讨论】:

  • 我可以重现这个。有趣的!此外,如果您在模块中包含throwIfNegative,并标有NOINLINE,则会失败。

标签: exception optimization haskell ghc hunit


【解决方案1】:

优化、严格性和imprecise exceptions 可能有点棘手。

重现上述问题的最简单方法是在throwIfNegative 上使用NOINLINE(该函数也没有跨模块边界内联):

import Control.Exception
import Test.HUnit

throwIfNegative :: Int -> String
throwIfNegative n | n < 0     = error "negative"
                  | otherwise = "no worries"
{-# NOINLINE throwIfNegative #-}

case_negative =
    handleJust errorCalls (const $ return ()) $ do
        evaluate $ throwIfNegative (-1)
        assertFailure "must throw when given a negative number"
  where errorCalls (ErrorCall _) = Just ()

main = runTestTT $ TestCase case_negative

阅读核心,优化后,GHC 正确内联evaluate (?):

catch#
      @ ()
      @ SomeException
      (\ _ ->
         case throwIfNegative (I# (-1)) of _ -> ...

然后在案件审查员之外调用throwIfError

lvl_sJb :: String
lvl_sJb = throwIfNegative lvl_sJc

lvl_sJc = I# (-1)

throwIfNegative =
  \ (n_adO :: Int) ->
    case n_adO of _ { I# x_aBb ->
      case <# x_aBb 0 of _ {
         False -> lvl_sCw; True -> error lvl_sCy

奇怪的是,此时,没有其他代码调用lvl_sJb,所以整个测试变成了死代码,并被剥离——GHC 已确定它未被使用!

使用seq 而不是evaluate 就足够了:

case_negative =
    handleJust errorCalls (const $ return ()) $ do
        throwIfNegative (-1) `seq` assertFailure "must throw when given a negative number"
  where errorCalls (ErrorCall _) = Just ()

或爆炸模式:

case_negative =
    handleJust errorCalls (const $ return ()) $ do
        let !x = throwIfNegative (-1)
        assertFailure "must throw when given a negative number"
  where errorCalls (ErrorCall _) = Just ()

所以我认为我们应该看看evaluate的语义:

-- | Forces its argument to be evaluated to weak head normal form when
-- the resultant 'IO' action is executed. It can be used to order
-- evaluation with respect to other 'IO' operations; its semantics are
-- given by
--
-- >   evaluate x `seq` y    ==>  y
-- >   evaluate x `catch` f  ==>  (return $! x) `catch` f
-- >   evaluate x >>= f      ==>  (return $! x) >>= f
--
-- /Note:/ the first equation implies that @(evaluate x)@ is /not/ the
-- same as @(return $! x)@.  A correct definition is
--
-- >   evaluate x = (return $! x) >>= return
--
evaluate :: a -> IO a
evaluate a = IO $ \s -> let !va = a in (# s, va #) -- NB. see #2273

#2273 bug 读起来很有趣。

我认为 GHC 在这里做了一些可疑的事情,建议不要使用evalaute(而是直接使用seq)。这需要更多地思考GHC在严格性方面做了什么。

我已经 filed a bug report 帮助获得 GHC 总部的决定。

【讨论】:

  • 非常有趣。我会留意跟踪的。
猜你喜欢
  • 2013-12-31
  • 2013-12-04
  • 1970-01-01
  • 2016-08-17
  • 1970-01-01
  • 2014-10-07
  • 2021-04-11
  • 1970-01-01
  • 2014-11-05
相关资源
最近更新 更多