问题描述
在 Haskell 中,您可以从纯函数代码中抛出异常,但只能在 IO 代码中捕获.
In Haskell, you can throw an exception from purely functional code, but you can only catch in IO code.
- 为什么?
- 你能在其他上下文中捕捉还是只能在 IO monad 中捕捉?
- 其他纯函数式语言如何处理它?
- Why?
- Can you catch in other contexts or only the IO monad?
- How do other purely functional languages handle it?
推荐答案
因为抛出一个函数内的异常不会使该函数的结果依赖于除参数值和定义之外的任何东西功能;函数保持纯净.OTOH 捕获函数内的异常确实(或至少可以)使该函数不再是纯函数.
Because throwing an exception inside a function doesn't make that function's result dependent on anything but the argument values and the definition of the function; the function remains pure. OTOH catching an exception inside a function does (or at least can) make that function no longer a pure function.
我将研究两种异常.第一个是不确定的;此类异常在运行时不可预测地出现,包括内存不足错误等.这些异常的存在不包含在可能生成它们的函数的含义中.它们只是我们必须处理的生活中令人不快的事实,因为我们在现实世界中有实际的物理机器,它们并不总是与我们用来帮助我们对其进行编程的抽象相匹配.
I'm going to look at two kinds of exceptions. The first is nondeterministic; such exceptions arise unpredictably at runtime, and include things like out of memory errors. The existence of these exceptions is not included in the meaning of the functions that might generate them. They're just an unpleasant fact of life we have to deal with because we have actual physical machines in the real world, which don't always match up to the abstractions we're using to help us program them.
如果一个函数抛出这样的异常,这意味着对函数求值的一次特定尝试未能产生一个值.这并不一定意味着函数的结果是未定义的(在这次调用的参数上),但系统无法产生结果.
If a function throws such an exception, it means that that one particular attempt to evaluate the function failed to produce a value. It doesn't necessarily mean that the function's result is undefined (on the arguments it was invoked on this time), but the system was unable to produce the result.
如果你能在纯调用者中捕获这样的异常,你可以做一些事情,比如让一个函数在子计算成功完成时返回一个(非底部)值,当它用完内存时返回另一个值.这作为纯函数没有意义;由函数调用计算的值应该由其参数的值和函数的定义唯一确定.能够根据子计算是否耗尽内存而返回不同的东西,这使得返回值依赖于其他东西(物理机上有多少可用内存、正在运行的其他程序、操作系统及其策略等).);根据定义,可以以这种方式运行的函数不是纯函数,并且(通常)不能存在于 Haskell 中.
If you could catch such an exception within a pure caller, you could do things like have a function that returns one (non-bottom) value when a sub-computation completes successfully, and another when it runs out of memory. This doesn't make sense as a pure function; the value computed by a function call should be uniquely determined by the values of its arguments and the definition of the function. Being able to return something different depending on whether the sub-computation ran out of memory makes the return value dependent on something else (how much memory is available on the physical machine, what other programs are running, the operating system and its policies, etc.); by definition a function which can behave this way is not pure and can't (normally) exist in Haskell.
由于纯粹的操作失败,我们确实必须允许评估一个函数可能会产生底部而不是它应该"产生的值.这并没有完全破坏我们对 Haskell 程序的语义解释,因为我们知道底部会导致所有调用者也产生底部(除非他们不需要应该计算的值,但在这种情况下非-严格评估意味着系统永远不会尝试评估此功能并失败).这听起来很糟糕,但是当我们将计算放在 IO
monad 中时,我们可以安全地捕获此类异常.IO
monad 中的值允许依赖于程序外部"的事物;事实上,它们可以根据世界上的任何东西改变它们的值(这就是为什么对 IO
值的一种常见解释是,它们好像传递了整个宇宙的表示).因此,如果纯子计算内存不足,则 IO
值有一个结果是完全可以的,如果没有,则有另一个结果.
Because of purely operational failures, we do have to allow that evaluating a function may produce bottom instead of the value it "should" have produced. That doesn't completely ruin our semantic interpretation of Haskell programs, because we know the bottom will cause all the callers to produce bottom as well (unless they didn't need the value that was supposed to be computed, but in that case non-strict evaluation implies that the system never would have tried to evaluate this function and failed). That sounds bad, but when we place our computation inside the IO
monad than we can safely catch such exceptions. Values in the IO
monad are allowed to depend on things "outside" the program; in fact they can change their value dependent on anything in the world (this is why one common interpretation of IO
values is that they are as if they were passed a representation of the entire universe). So it's perfectly okay for an IO
value to have one result if a pure sub-computation runs out of memory and another result if it doesn't.
但是确定性异常呢?在这里,我谈论的是在对特定参数集评估特定函数时总是 抛出的异常.此类异常包括被零除错误,以及从纯函数显式抛出的任何异常(因为它的结果只能取决于它的参数和它的定义,如果它评估为一次抛出,它总是em> 对相同的参数求得相同的 throw[1]).
But what about deterministic exceptions? Here I'm talking about exceptions that are always thrown when evaluating a particular function on a particular set of arguments. Such exceptions include divide-by-zero errors, as well as any exception explicitly thrown from a pure function (since its result can only depend on its arguments and its definition, if it evaluates to a throw once it will always evaluate to the same throw for the same arguments[1]).
看起来这类异常应该可以在纯代码中捕获.毕竟,1/0
的值只是被零除的错误.如果一个函数可以有不同的结果取决于子计算是否通过检查它是否传入零来评估为被零除错误,为什么它不能通过检查结果是否是除法来做到这一点-零错误?
It might seem like this class of exceptions should be catchable in pure code. After all, the value of 1 / 0
just is a divide-by-zero error. If a function can have a different result depending on whether a sub-computation evaluates to a divide-by-zero error by checking whether it's passing in a zero, why can't it do this by checking whether the result is a divide-by-zero error?
这里我们回到 larsmans 在评论中提出的观点.如果一个纯函数可以观察从throw ex1 + throw ex2
得到哪个异常,那么它的结果就取决于执行的顺序.但这取决于运行时系统,可以想象它甚至可以在同一系统的两个不同执行之间发生变化.也许我们有一些先进的自动并行化实现,它在每次执行时尝试不同的并行化策略,以便尝试在多次运行中收敛到最佳策略.这将使异常捕获功能的结果取决于所使用的策略、机器中的 CPU 数量、机器上的负载、操作系统及其调度策略等.
Here we get back to the point larsmans made in a comment. If a pure function can observe which exception it gets from throw ex1 + throw ex2
, then its result becomes dependent on the order of execution. But that's up to the runtime system, and it could conceivably even change between two different executions of the same system. Maybe we've got some advanced auto-parallelising implementation which tries different parallelisation strategies on each execution in order to try to converge on the best strategy over multiple runs. This would make the result of the exception-catching function depend on the strategy being used, the number of CPUs in the machine, the load on the machine, the operating system and its scheduling policies, etc.
同样,纯函数的定义是只有通过参数(及其定义)进入函数的信息才会影响其结果.在非IO
函数的情况下,影响抛出哪个异常的信息不会通过其参数或定义进入函数,因此它不会对结果产生影响.但是 IO
monad 中的计算可以隐式地依赖于整个宇宙的任何细节,因此在那里捕获此类异常是没问题的.
Again, the definition of a pure function is that only information which comes into a function through its arguments (and its definition) should affect its result. In the case of non-IO
functions, the information affecting which exception gets thrown doesn't come into the function through its arguments or definition, so it can't have an effect on the result. But computations in the IO
monad implicitly are allowed to depend on any detail of the entire universe, so catching such exceptions is fine there.
至于您的第二个点:不,其他 monad 不能用于捕获异常.所有相同的论点都适用;产生 Maybe x
或 [y]
的计算不应该依赖于它们的参数之外的任何东西,并且捕捉任何类型的异常泄漏"关于事物的各种细节不包含在这些函数参数中.
As for your second dot point: no, other monads wouldn't work for catching exceptions. All the same arguments apply; computations producing Maybe x
or [y]
aren't supposed to depend on anything outside their arguments, and catching any kind of exception "leaks" all sorts of details about things which aren't included in those function arguments.
请记住,monad 没有什么特别之处.它们的工作方式与 Haskell 的其他部分没有任何不同.monad 类型类是在普通的 Haskell 代码中定义的,几乎所有的 monad 实现也是如此.所有 适用于普通 Haskell 代码的相同规则适用于所有 monad.IO
本身很特别,而不是它是一个 monad.
Remember, there's nothing particularly special about monads. They don't work any differently than other parts of Haskell. The monad typeclass is defined in ordinary Haskell code, as are almost all monad implementations. All the same rules that apply to ordinary Haskell code apply to all monads. It's IO
itself that is special, not the fact that it's a monad.
至于其他纯语言如何处理异常捕获,我体验过的唯一具有强制纯度的其他语言是 Mercury.[2]Mercury 的处理方式与 Haskell 略有不同,您可以在纯代码中捕获异常.
As for how other pure languages handle exception catching, the only other language with enforced purity that I have experience with is Mercury.[2] Mercury does it a little differently from Haskell, and you can catch exceptions in pure code.
Mercury 是一种逻辑编程语言,因此Mercury 程序不是基于函数构建的,而是基于谓词构建的;对谓词的调用可以有零个、一个或多个解决方案(如果您熟悉在列表 monad 中编程以获得不确定性,这有点像整个语言都在列表 monad 中).在操作上,Mercury 执行使用回溯来递归枚举谓词的所有可能的解决方案,但非确定性谓词的语义是它简单地为每组输入参数提供一组解决方案,而不是一个 Haskell 函数,它为每组输入参数计算一个结果值.与 Haskell 一样,Mercury 是纯的(包括 I/O,但它使用的机制略有不同),因此对谓词的每次调用都必须唯一确定一个 解决方案集,这仅取决于参数和谓词的定义.
Mercury is a logic programming language, so rather than being built on functions, Mercury programs are built from predicates; a call to a predicate can have zero, one, or more solutions (if you're familiar with programming in the list monad to get nondeterminism, it's a little bit like the entire language is in the list monad). Operationally, Mercury execution uses backtracking to recursively enumerate all possible solutions to a predicate, but the semantics of a nondeterministic predicate is that it simply has a set of solutions for each set of its input arguments, as opposed to a Haskell function which calculates a single result value for each set of its input arguments. Like Haskell, Mercury is pure (including I/O, though it uses a slightly different mechanism), so each call to a predicate must uniquely determine a single solution set, which depends only on the arguments and the definition of the predicate.
Mercury 跟踪每个谓词的确定性".总是只产生一种解决方案的谓词称为 det
(确定性的缩写).那些产生至少一个解决方案的被称为multi
.还有一些其他确定性类,但它们在这里不相关.
Mercury tracks the "determinism" of each predicate. Predicates which always result in exactly one solution are called det
(short for deterministic). Those which generate at least one solution are called multi
. There are a few other determinism classes as well, but they're not relevant here.
使用 try
块捕获异常(或通过显式调用实现它的高阶谓词)具有确定性 cc_multi
.cc 代表承诺的选择".这意味着这个计算至少有一个解决方案,并且在操作上程序只会得到其中一个".这是因为运行子计算并查看它是否产生异常有一个解决方案集,它是子计算的正常"解决方案加上它可能抛出的所有可能异常的集合.由于所有可能的异常"包括所有可能的运行时故障,其中大部分永远不会真正发生,因此无法完全实现此解决方案集.执行引擎实际上不可能回溯到 try
块的所有可能的解决方案,因此它只是为您提供一个解决方案(正常的解决方案或指示探索了所有的可能性,没有解决方案或例外,或者第一个偶然出现的例外).
Catching an exception with a try
block (or by explicitly calling the higher-order predicates which implement it) has determinism cc_multi
. The cc stands for "committed choice". It means "this computation has at least one solution, and operationally the program is only going to get one of them". This is because running the sub-computation and seeing whether it produced an exception has a solution set which is the union of the sub-computation's "normal" solutions plus the set of all possible exceptions it could throw. Since "all possible exceptions" includes every possible runtime failure, most of which will never actually happen, this solution set can't be fully realised. There's no possible way the execution engine could actually backtrack through every possible solution to the try
block, so instead it just gives you a solution (either a normal one, or an indication that all possibilities were explored and there was no solution or exception, or the first exception that happened to arise).
因为编译器会跟踪确定性,所以它不允许您在完整解决方案集很重要的上下文中调用 try
.例如,您不能使用它来生成没有遇到异常的所有解决方案,因为编译器会抱怨它需要 cc_multi
调用的所有解决方案,而该调用只会产生一个.但是,您也不能从 det
谓词中调用它,因为编译器会抱怨 det
谓词(应该只有一个解决方案)正在制作 cc_multi
调用,它将有多个解决方案(我们只会知道其中一个是什么).
Because the compiler keeps track of the determinism, it will not allow you to call try
in a context where the complete solution set matters. You can't use it to generate all solutions which don't encounter an exception, for example, because the compiler will complain that it needs all solutions to a cc_multi
call, which is only going to produce one. However you also can't call it from a det
predicate, because the compiler will complain that a det
predicate (which is supposed to have exactly one solution) is making a cc_multi
call, which will have multiple solutions (we're just only going to know what one of them is).
那么这到底有什么用呢?好吧,您可以将 main
(以及它调用的其他东西,如果有用的话)声明为 cc_multi
,并且它们可以毫无问题地调用 try
.这意味着整个程序理论上有多个解决方案",但运行它会生成一个解决方案.这允许您编写一个程序,当它碰巧在某个时刻用完内存时,它的行为会有所不同.但这并没有破坏声明式语义,因为它会用更多可用内存计算出的真实"结果仍然在解决方案集中(就像内存不足异常仍然在当程序实际计算一个值时设置的解决方案),只是我们最终只能得到一个任意的解决方案.
So how on earth is this useful? Well, you can have main
(and other things it calls, if that's useful) declared as cc_multi
, and they can call try
with no problems. This means that the entire program has multiple "solutions" in theory, but running it will generate a solution. This allows you to write a program that behaves differently when it happens to run out of memory at some point. But it doesn't spoil the declarative semantics because the "real" result it would have computed with more memory available is still in the solution set (just as the out-of-memory exception is still in the solution set when the program actually does compute a value), it's just that we only end up with one arbitrary solution.
det
(只有一种解决方案)与 cc_multi
(有多种解决方案,但您只能拥有其中一种)的处理方式不同,这一点很重要.类似于在 Haskell 中捕获异常的推理,异常捕获不能发生在非提交选择"的上下文中,或者您可以获得纯谓词,根据来自现实世界的信息,它们应该产生不同的解决方案集.t有权访问.try
的 cc_multi
确定性允许我们编写程序就好像他们产生了无限的解决方案集(主要是不太可能的异常的小变种),并阻止我们编写实际上需要多个解决方案的程序.[3]
It's important that det
(there is exactly one solution) is treated differently from cc_multi
(there are multiple solutions, but you can only have one of them). Similarly to the reasoning about catching exceptions in Haskell, exception catching can't be allowed to happen in a non-"committed choice" context, or you could get pure predicates producing different solution sets depending on information from the real world that they shouldn't have access to. The cc_multi
determinism of try
allows us to write programs as if they produced an infinite solution set (mostly full of minor variants of unlikely exceptions), and prevents us from writing programs that actually need more than one solution from the set.[3]
[1] 除非评估它首先遇到不确定性错误.现实生活很痛苦.
[1] Unless evaluating it encounters a nondeterministic error first. Real life's a pain.
[2] 仅仅鼓励程序员使用纯度而不强制它的语言(例如 Scala)往往只是让您随时随地捕获异常,就像它们允许您随时随地进行 I/O 一样.
[2] Languages which merely encourage the programmer to use purity without enforcing it (such as Scala) tend to just let you catch exceptions wherever you want, same as they allow you to do I/O wherever you want.
[3] 请注意,承诺选择"的概念不是 Mercury 处理纯 I/O 的方式.为此,Mercury 使用独特的类型,它与承诺选择"确定性类正交.
[3] Note that the "committed choice" concept is not how Mercury handles pure I/O. For that, Mercury uses unique types, which is orthogonal to the "committed choice" determinism class.
这篇关于为什么捕获异常是非纯的,而抛出异常是纯的?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!