Haskell中的异常是如何工作的(第二部分)?

17

我有以下代码:

{-# LANGUAGE DeriveDataTypeable #-}
import Prelude hiding (catch)
import Control.Exception (throwIO, Exception)
import Control.Monad (when)
import Data.Maybe
import Data.Word (Word16)
import Data.Typeable (Typeable)
import System.Environment (getArgs)

data ArgumentParserException = WrongArgumentCount | InvalidPortNumber
    deriving (Show, Typeable)

instance Exception ArgumentParserException

data Arguments = Arguments Word16 FilePath String

main = do
    args <- return []
    when (length args /= 3) (throwIO WrongArgumentCount)

    let [portStr, cert, pw] = args
    let portInt = readMaybe portStr :: Maybe Integer
    when (portInt == Nothing) (throwIO InvalidPortNumber)

    let portNum = fromJust portInt
    when (portNum < 0 || portNum > 65535) (throwIO InvalidPortNumber)

    return $ Arguments (fromInteger portNum) cert pw

-- Newer 'base' has Text.Read.readMaybe but alas, that doesn't come with
-- the latest Haskell platform, so let's not rely on it
readMaybe :: Read a => String -> Maybe a
readMaybe s = case reads s of
    [(x, "")] -> Just x
    _         -> Nothing

当编译器开启或关闭优化时,其行为会有所不同:

crabgrass:~/tmp/signserv/src% ghc -fforce-recomp Main.hs && ./Main
Main: WrongArgumentCount
crabgrass:~/tmp/signserv/src% ghc -O -fforce-recomp Main.hs && ./Main
Main: Main.hs:20:9-34: Irrefutable pattern failed for pattern [portStr, cert, pw]

为什么会这样呢?我知道不准确的例外可以随意选择;但是在这里,我们选择了一个精确的例外和一个不准确的例外,所以那个警告不适用。

这对我来说看起来像是一个bug。你使用的是哪个GHC版本?我在GHC 7.6.2中看到了相同的行为。 - hammar
@hammar,这个问题至少在7.6.1和7.4.1版本中出现,在#haskell中提出此问题的人使用的是7.0.x版本。 - Daniel Wagner
@DanielFischer 嗯,我的记忆可能有误。无论如何,我知道它既不是7.4.1也不是7.6.1,因为我不得不更改他发送给我的cabal文件以放宽对base的依赖关系。=) - Daniel Wagner
对我来说,它已经改为7.2了,所以可能是7.2.x版本。 - Daniel Fischer
2
我已经能够将它简化为这个更短的例子(http://hpaste.org/88323)。 - hammar
显示剩余2条评论
1个回答

14

我同意hammar的想法,这看起来像是一个bug。而且似乎自一段时间以来在HEAD中已经被修复了。在旧版的ghc-7.7.20130312和今天的HEAD ghc-7.7.20130521中,会引发WrongArgumentCount异常并删除main的所有其他代码(优化器的恶作剧)。然而,在7.6.3中仍然存在问题。

行为在7.2系列中发生了改变,我从7.0.4中得到了预期的WrongArgumentCount,(经过优化的)核心也表明了这一点:

Main.main1 =
  \ (s_a11H :: GHC.Prim.State# GHC.Prim.RealWorld) ->
    case GHC.List.$wlen
           @ GHC.Base.String (GHC.Types.[] @ GHC.Base.String) 0
    of _ {
      __DEFAULT ->
        case GHC.Prim.raiseIO#
               @ GHC.Exception.SomeException @ () Main.main7 s_a11H
        of _ { (# new_s_a11K, _ #) ->
        Main.main2 new_s_a11K
        };
      3 -> Main.main2 s_a11H
    }

当空列表的长度与3不同时,引发WrongArgumentCount异常,否则尝试执行其余操作。

从7.2版本开始,长度的计算被移至解析portStr之后:

Main.main1 =
  \ (eta_Xw :: GHC.Prim.State# GHC.Prim.RealWorld) ->
    case Main.main7 of _ {
      [] -> case Data.Maybe.fromJust1 of wild1_00 { };
      : ds_dTy ds1_dTz ->
        case ds_dTy of _ { (x_aOz, ds2_dTA) ->
        case ds2_dTA of _ {
          [] ->
            case ds1_dTz of _ {
              [] ->
                case GHC.List.$wlen
                       @ [GHC.Types.Char] (GHC.Types.[] @ [GHC.Types.Char]) 0
                of _ {
                  __DEFAULT ->
                    case GHC.Prim.raiseIO#
                           @ GHC.Exception.SomeException @ () Main.main6 eta_Xw
                    of wild4_00 {
                    };
                  3 ->

在哪里

Main.main7 =
  Text.ParserCombinators.ReadP.run
    @ GHC.Integer.Type.Integer Main.main8 Main.main3

Main.main8 =
  GHC.Read.$fReadInteger5
    GHC.Read.$fReadInteger_$sconvertInt
    Text.ParserCombinators.ReadPrec.minPrec
    @ GHC.Integer.Type.Integer
    (Text.ParserCombinators.ReadP.$fMonadP_$creturn
       @ GHC.Integer.Type.Integer)

Main.main3 = case lvl_r1YS of wild_00 { }

lvl_r1YS =
  Control.Exception.Base.irrefutPatError
    @ ([GHC.Types.Char], [GHC.Types.Char], [GHC.Types.Char])
    "Except.hs:21:9-34|[portStr, cert, pw]"

由于throwIO应该遵守IO操作的顺序,因此应优先使用throwIO变体来在IO单子中引发异常,因为它保证了与其他IO操作的顺序,而throw则没有。

当内联程序看到when除了可能抛出异常以外什么也不做时,您可以通过使用NOINLINE的变体或在抛出之前执行有副作用的IO动作来强制正确排序,因此似乎次序并不重要。

这种情况不应该发生。

(不好意思,这不是一个真正的答案,但尝试将其适合于评论中;)


1
谢谢。在这里提交了一个bug报告(http://hackage.haskell.org/trac/ghc/ticket/7924);我们将看一下GHC开发人员是否同意。 - Daniel Wagner
你可能会发现它已经被修复了,我刚试了HEAD,结果符合预期。 - Daniel Fischer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接