5

モジュールに次のような関数があります。

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

これは、最適化が異なる方法で適用されるためだと思います。ポインタはありますか?

4

1 に答える 1

8

最適化、厳密性、および不正確な例外は、少し扱いに​​くい場合があります。

上記の問題を再現する最も簡単な方法は、NOINLINEonを使用するthrowIfNegativeことです (関数はモジュールの境界を越えてインライン化されていません):

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 バグは非常に興味深い読み物です。

GHC はここで怪しいことをしていると思うので、使用しないことをお勧めしevalauteます (代わりに、seq直接使用してください)。これは、GHC が厳密に何をしているのかをもっと考える必要があります。

GHC HQ から判断を得るために、バグ レポートを提出しました。

于 2011-04-18T00:21:38.050 に答える