如何捕获实例化特定 class 的所有异常?

How to catch all exceptions instantiating specific class?

我正在尝试实现类似于Java/C#的捕获异常的表现力,我可以在其中指定要捕获的异常的接口,否则我需要枚举所有可能的类型。

interface I {void f();}
class AE extends Exception implements I {}
class BE extends Exception implements I {}
try {
  throw (new Random().next() % 2 == 0 
          ? new AE() 
          : new BE());   
} catch (I e) {
  e.f();
}
class I e where f :: e -> IO ()
data AE = AE deriving (Show)
data BE = BE deriving (Show)
instance Exception AE
instance Exception BE
instance I AE where f _ = putStrLn "f AE"
instance I BE where f _ = putStrLn "f BE"

run m = try @(forall e . (I e, Exception e) => e) m >>= \case
    Left er -> f er
    Right () -> pure ()

编译器抱怨:

  GHC doesn't yet support impredicative polymorphism 

原错误由ghc 8.10.7产生。

GHC 9.2.1 已经发布。 打开 ImpredicitveTypes 后,错误是不同的:

    • No instance for (Exception (forall e. I e => e))
        arising from a use of ‘try’

我再次取消删除这个答案,但请先阅读


正如我已经评论过的,这可能是完全错误的方法,您应该改为使用单一异常类型

data I = AE | BE ...

但是,如果您坚持拥有开放类型-class 功能,这种类型也可以是存在的(这基本上是 OO 引用到基础-class ).请注意 existentials should not be used 除非你真的有充分的理由这样做。

{-# LANGUAGE GADTs, LambdaCase, StandaloneDeriving #-}

import Control.Exception


class I e where f :: e -> IO ()

data AE = AE deriving (Show)
instance I AE where f _ = putStrLn "f AE"
data BE = BE deriving (Show)
instance I BE where f _ = putStrLn "f BE"

data AnI where
  AnI :: (I e, Show e) => e -> AnI
deriving instance Show AnI
instance Exception AnI

run m = try m >>= \case
    Left (AnI er) -> f er
    Right () -> pure ()

main :: IO ()
main = run (throw (AnI BE))

exception-via 库似乎解决了显式包装这些存在的尴尬,并允许创建实际的 层次结构 异常。我还没有尝试过图书馆,但看起来很有希望。


另一种方法,它允许您在没有包装器的情况下抛出“原始”类型,但另一方面需要先验列出所有支持的异常(但实际上只是列出它们), 是

{-# LANGUAGE DataKinds, KindSignatures, ScopedTypeVariables, UnicodeSyntax
           , MultiParamTypeClasses, FlexibleInstances, ConstraintKinds
           , AllowAmbiguousTypes
           , TypeApplications, RankNTypes, DeriveAnyClass, TypeOperators #-}

import Control.Exception
import Data.Kind


class PolyExcept (c :: Type -> Constraint) (l :: [Type]) where
  handleAll :: (∀ e . c e => e -> IO a) -> IO a -> IO a

instance PolyExcept c '[] where
  handleAll _ = id

instance ∀ c e l . (Exception e, c e, PolyExcept c l)
            => PolyExcept c (e ': l) where
  handleAll h a = handle @e h (handleAll @c @l h a)

class I e where f :: e -> IO ()

data AE = AE deriving (Show, Exception)
instance I AE where f _ = putStrLn "f AE"
data BE = BE deriving (Show, Exception)
instance I BE where f _ = putStrLn "f BE"

run :: IO () -> IO ()
run = handleAll @I @'[AE, BE] f

main :: IO ()
main = run (throw BE)

@leftaroundabout(现已删除)的回答很好地解释了基础知识,我认为应该取消删除。但我添加这个答案是为了解释“标准”GHC 异常系统 (see also docs)。

首先你有 Exception class,你所有的异常都应该实现它。你不知道的是,当你抛出一个异常时,它实际上被包裹在 SomeException 中,而实际上抛出的是那个 SomeException 值。

SomeException 定义为存在性包装 Exception class:

data SomeException where
  SomeException :: Exception e => e -> SomeException

并且通过 Exception class 的 toException 方法进行换行。在您发布的代码中,您在未定义任何方法的情况下实现 Exception 实例,因此使用默认实现。 toException 的默认实现只是直接包装:

toException e = SomeException e

Q: 好的,这很酷,但是当我 catch 这样的异常时,我不会只得到一个类型 SomeException?那有什么用?

啊,但这只是故事的一半!当您 捕获 异常时,会发生反向解包: fromException 方法解包 SomeException 包装器,然后查看其中的值是否与您尝试的类型匹配抓住。

问: 等一下! “查看值是否与类型匹配”?你刚才不是说Haskell里面没有运行时类型信息吗?

嗯,有。有点。只在你需要的时候。

It's called Typeable。这是一个你不必实现或派生的魔法 class,但 GHC 会在你需要时为你创建它的一个实例。它恰好(好吧,它实际上是故意的)Exception class 有 Typeable 作为超级 class,这意味着任何实现 Exception 的类型都有也实施 Typeable。除了您不必实际 实现 Typeable,编译器会为您完成。

Typeable 唯一真正允许做的是获得一种“类型 ID”——一个唯一标识特定类型的不透明值。然后您可以使用这些值进行比较,从而确定给定的通用值是否属于特定类型。整个比较 + 强制业务被整齐地包裹在 function called cast 中。你给它一个值和一个目标类型,它比较它们从 Typeable 获得的“类型 ID”(所以值和目标类型都必须实现它),然后 returns 你一样值,但强制转换为目标类型。或者不是,如果结果是不同的类型。所以它returns一个Maybe.

这就是 SomeException 解包基本上发生在 fromException 中的方式,这是 Exception 的第二种方法:

fromException :: Exception e => SomeException -> Maybe e
fromException (SomeException x) = cast x

记住:每次捕获异常时都会发生这种情况。基本上 catch 函数调用 fromException,然后如果结果是 Just 则调用你的处理程序(意味着异常是你的类型)或者如果它是 Nothing 则不调用。 =99=]

问: 好吧,那更酷了,但这并没有解决我的问题:我真的需要捕捉每一个特定类型的异常吗?我不能以某种方式一次抓住它们吗?

是的,你可以!您可以使用整个框架为自己设置一个存在主义的 层次结构 。比如说,我们想要一个如下所示的层次结构:

就像好的 OOP 一样! :-)

为了做到这一点,MyException 将是另一个存在主义,就像 SomeException 本身一样:

data MyException where
  MyException :: forall e. Exception e => MyException

instance Exception MyException

然后确保在抛出异常时发生的“包装”过程中,AEBE 在包装在 SomeException:

instance Exception AE where
  toException e = SomeException $ MyException e

-- Same implementation for BE

同样,当捕获时发生“展开”时,请确保 AEBE 都可以展开自己,方法是首先确保 MyException 包裹在 SomeException,然后 AEBE 分别包裹在 MyException:

instance Exception AE where
  fromException x = do
    MyException m <- cast x
    cast m

-- Same implementation for BE

当然,这样做有点脆弱:AEBE 不仅要知道他们自己的“祖先”类型 MyException,还要知道它的祖先输入 SomeException。所以在实践中 MyException 的包装和解包通常委托给 MyException 自己的 Exception 实例:

instance Exception AE where
  toException e = toException $ MyException e

  fromException x = do
    MyException m <- fromException x
    cast m

瞧:现在您可以单独捕获 AEBEMyException,这对它们都适用。为什么?因为如果你正在捕获 MyException,当 SomeException 被解包时,MyExceptionfromException 实现将用于解包。但是,如果您要捕获 AE,那么将使用 AEfromException 的实现,首先解包 MyException,然后 AE 本身。

Q: 好的,太好了,现在我明白了,但仍然:我可以查询实现某种类型的异常吗class?

是也不是。

如果你只是有一个任意值,你无法判断它是否实现了某种类型 class,我们称它为 I。因为 Haskell 没有运行时类型信息,除非你特别要求它,即使那样,Typeable 也只能回答“这是 X 类型的值吗?”

但不仅如此:“这个值是否实现 class I”甚至不是一个有效的问题,因为它取决于范围。在导入 I 实现模块的模块中,您确实有这样的实例。在没有此类导入的模块中,实例不存在。您甚至可以在不同的模块中为同一类型设置 两个不同的 实例 I,这些实例在程序的不同部分导入。当然,这是一种不好的做法,不要这样做,但它 可以 发生。

这里问题的根源在于,与 OOP 接口不同,class 实例不是类型本身的 属性,而是一种类型(或几种类型)与class.

但是!你还是可以的。

你可以做的是要求任何从 MyException“继承”(见上图)的异常必须具有某个 class:

的实例
data MyException where
  MyException :: (Exception e, I e) => e -> MyException

现在任何想要将值包装在 MyException 中的人都必须提供一个实例 I e,该实例将包装在 MyException 值中,您将获得访问权限当你打开它时。例如:

class I a where 
  i :: a -> String

instance I AE where
  i _ = "This is AE"

throw AE `catch` \(MyException e) -> purStrLn (i e)

我可以在处理程序中使用方法 i,因为我刚刚解包 MyException 并从中得到一个 I 字典。

但重要的一点是:字典不是凭空而来的。它是由首先抛出异常的人放入 MyException 值中的。


切线:您实际上可以劫持(某种程度上)异常 wrapping/unwrapping 机制以允许捕获尚未抛出的类型。您所做的就是对 fromException:

进行“不诚实”的实施
data Hijacked = Hijacked deriving Show

instance Exception Hijacked where
  fromException x = do
    AE <- fromException x  -- See if it's an AE
    pure Hijacked

throw AE `catch` \Hijacked -> putStrLn "I caught a Hijacked!"