2011-04-17 79 views
4

我有一個模塊中的功能,看起來是這樣的:優化和測試誤差之間的相互作用要求

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
  • 類型簽名

我認爲這是因爲它導致優化應用不同。任何指針?

+2

我可以重現這一點。有趣!另外,如果在模塊中包含'throwIfNegative',標記爲'NOINLINE',則失敗。 – 2011-04-17 23:39:36

回答

7

優化,嚴格性和imprecise exceptions可能有點棘手。

重現上述這個問題的最簡單的方法是用一個NOINLINEthrowIfNegative(功能不被跨越模塊邊界任一內聯):

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,案件scrutinizer之外:

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是一個prett有趣的閱​​讀。

我認爲GHC在這裏做可疑的事情,並建議不要使用evalaute(而是直接使用seq)。這需要更多地考慮GHC在嚴格性方面正在做什麼。

我已經filed a bug report幫助從GHC總部得到一個決心。

+1

非常有趣。我會密切關注trac。 – hammar 2011-04-18 00:30:05