如何捕获实例化特定 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
然后确保在抛出异常时发生的“包装”过程中,AE
和 BE
在包装在 SomeException
:
instance Exception AE where
toException e = SomeException $ MyException e
-- Same implementation for BE
同样,当捕获时发生“展开”时,请确保 AE
和 BE
都可以展开自己,方法是首先确保 MyException
包裹在 SomeException
,然后 AE
或 BE
分别包裹在 MyException
:
中
instance Exception AE where
fromException x = do
MyException m <- cast x
cast m
-- Same implementation for BE
当然,这样做有点脆弱:AE
和 BE
不仅要知道他们自己的“祖先”类型 MyException
,还要知道它的祖先输入 SomeException
。所以在实践中 MyException
的包装和解包通常委托给 MyException
自己的 Exception
实例:
instance Exception AE where
toException e = toException $ MyException e
fromException x = do
MyException m <- fromException x
cast m
瞧:现在您可以单独捕获 AE
或 BE
或 MyException
,这对它们都适用。为什么?因为如果你正在捕获 MyException
,当 SomeException
被解包时,MyException
的 fromException
实现将用于解包。但是,如果您要捕获 AE
,那么将使用 AE
对 fromException
的实现,首先解包 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!"
我正在尝试实现类似于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
然后确保在抛出异常时发生的“包装”过程中,AE
和 BE
在包装在 SomeException
:
instance Exception AE where
toException e = SomeException $ MyException e
-- Same implementation for BE
同样,当捕获时发生“展开”时,请确保 AE
和 BE
都可以展开自己,方法是首先确保 MyException
包裹在 SomeException
,然后 AE
或 BE
分别包裹在 MyException
:
instance Exception AE where
fromException x = do
MyException m <- cast x
cast m
-- Same implementation for BE
当然,这样做有点脆弱:AE
和 BE
不仅要知道他们自己的“祖先”类型 MyException
,还要知道它的祖先输入 SomeException
。所以在实践中 MyException
的包装和解包通常委托给 MyException
自己的 Exception
实例:
instance Exception AE where
toException e = toException $ MyException e
fromException x = do
MyException m <- fromException x
cast m
瞧:现在您可以单独捕获 AE
或 BE
或 MyException
,这对它们都适用。为什么?因为如果你正在捕获 MyException
,当 SomeException
被解包时,MyException
的 fromException
实现将用于解包。但是,如果您要捕获 AE
,那么将使用 AE
对 fromException
的实现,首先解包 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!"