Cra*_*lin 37 haskell functional-programming exception-handling purely-functional
在Haskell中,您可以从纯函数代码中抛出异常,但是您只能捕获IO代码.
Ben*_*Ben 50
因为在函数内部抛出异常并不会使该函数的结果依赖于除参数值和函数定义之外的任何内容; 功能仍然是纯粹的.OTOH 在函数内捕获异常确实(或至少可以)使该函数不再是纯函数.
我要看两种例外.第一个是不确定的; 这种异常在运行时出现不可预测的情况,包括内存不足错误等.这些异常的存在不包括在可能生成它们的函数的含义中.它们只是我们必须处理的生活中令人不快的事实,因为我们在现实世界中拥有实际的物理机器,这些机器并不总是与我们用来帮助我们编程的抽象相匹配.
如果函数抛出此类异常,则意味着评估函数的一次特定尝试无法生成值.它并不一定意味着函数的结果是未定义的(在此时调用的参数上),但系统无法生成结果.
如果你可以在纯调用者中捕获这样的异常,你可以做一些事情,比如有一个函数在子计算成功完成时返回一个(非底部)值,另一个函数在内存不足时返回.作为纯粹的功能,这没有意义; 函数调用计算的值应该由其参数的值和函数的定义唯一确定.根据子计算是否耗尽内存而能够返回不同的内容使得返回值依赖于其他内容(物理机上有多少可用内存,正在运行的其他程序,操作系统及其策略等)等等); 根据定义,可以以这种方式运行的函数不是纯粹的,并且不能(通常)存在于Haskell中.
由于纯操作失败,我们必须允许评估函数可能产生底部而不是它"应该"产生的值.这并没有完全破坏我们对Haskell程序的语义解释,因为我们知道底部也会导致所有调用者产生底部(除非他们不需要应该计算的值,但在这种情况下非严格的评估意味着系统永远不会尝试评估此功能并失败).这听起来很糟糕,但是当我们在IOmonad中进行计算时,我们可以安全地捕获这些异常.IOmonad 中的值可以取决于程序"外部"的内容; 事实上,他们可以根据世界上的任何事物改变他们的价值(这就是为什么对IO价值观的一种共同解释是他们就好像他们被传递了整个宇宙的代表一样).因此,IO如果纯子计算耗尽内存,则一个值得到一个结果是完全没问题的,如果没有则另一个结果.
但是确定性异常呢?这里我讨论的是在评估特定参数集上的特定函数时总是抛出的异常.这样的异常包括被零除错误,以及从纯函数显式抛出的任何异常(因为它的结果只能依赖于它的参数及其定义,如果它一直计算到一个throw就会一直计算到同一个throw对于相同的论点[1]).
看起来这类异常应该可以在纯代码中捕获.毕竟,1 / 0just 的值是一个被零除错误.如果一个函数可以有一个不同的结果,取决于子计算是否通过检查它是否通过零来评估为被零除错误,为什么它不能通过检查结果是否为除数来做到这一点 - 零错误?
在这里,我们回到larsmans在评论中提出的观点.如果纯函数可以观察它来自哪个异常throw ex1 + throw ex2,那么它的结果将取决于执行的顺序.但这取决于运行时系统,并且可以想象甚至可以在同一系统的两个不同执行之间进行更改.也许我们有一些先进的自动并行化实现,它在每次执行时尝试不同的并行化策略,以便尝试在多次运行中收敛最佳策略.这将使异常捕获函数的结果取决于所使用的策略,机器中的CPU数量,机器上的负载,操作系统及其调度策略等.
同样,纯函数的定义是只有通过其参数(及其定义)进入函数的信息才会影响其结果.在非IO函数的情况下,影响抛出异常的信息不会通过其参数或定义进入函数,因此它不会对结果产生影响.但是IOmonad中的计算隐含地允许依赖于整个Universe的任何细节,所以捕获这样的异常就好了.
至于你的第二个点,不,其他monad不能用于捕获异常.所有相同的论点都适用; 计算产生Maybe x或[y]不应该依赖于它们的参数之外的任何东西,并且捕获任何类型的异常"泄漏"关于那些未包含在那些函数参数中的事物的各种细节.
请记住,monads没有什么特别之处.它们与Haskell的其他部分没有任何不同.monad类型类是在普通的Haskell代码中定义的,几乎所有的monad实现都是如此.适用于普通Haskell代码的所有相同规则适用于所有monad.它IO本身就是特殊的,而不是它是一个monad的事实.
至于其他纯语言如何处理异常捕获,我所遇到的唯一具有强制纯度的语言是Mercury.[2] Mercury与Haskell的做法略有不同,你可以在纯代码中捕获异常.
Mercury是一种逻辑编程语言,因此Mercury程序不是基于函数构建的,而是根据谓词构建的; 对谓词的调用可以有零个,一个或多个解决方案(如果你熟悉列表monad中的编程以获得非确定性,那么它有点像整个语言在列表monad中).在操作上,Mercury执行使用回溯来递归枚举谓词的所有可能解决方案,但是非确定性谓词的语义是它只为每组输入参数提供一组解决方案,而不是计算单个Haskell函数的Haskell函数.每组输入参数的结果值.与Haskell一样,Mercury是纯粹的(包括I/O,尽管它使用稍微不同的机制),因此对谓词的每次调用都必须唯一地确定单个解决方案集,这仅取决于参数和谓词的定义.
水星追踪每个谓词的"决定论".总是产生一个解决方案的谓词被称为det(确定性的简称).产生至少一种解决方案的那些被称为multi.还有一些其他决定论类,但它们在这里并不相关.
使用try块捕获异常(或通过显式调用实现它的高阶谓词)具有确定性cc_multi.cc代表"坚定的选择".这意味着"这个计算至少有一个解决方案,在操作上程序只会得到其中一个".这是因为运行子计算并查看它是否产生异常有一个解决方案集,它是子计算的"正常"解决方案的联合加上它可能抛出的所有可能异常的集合.由于"所有可能的异常"包括每个可能的运行时故障,其中大部分将永远不会发生,因此无法完全实现此解决方案集.执行引擎无法实际回溯到try块的每个可能的解决方案,所以它只是给你一个解决方案(正常的解决方案,或指示所有可能性都被探索,没有解决方案或异常,或者发生的第一个例外).
因为编译器会跟踪确定性,所以它不允许您try在完整解决方案集很重要的上下文中调用.您不能使用它来生成所有不会遇到异常的解决方案,例如,因为编译器会抱怨它需要cc_multi调用的所有解决方案,这只会生成一个.但是你也不能从det谓词中调用它,因为编译器会抱怨det谓词(应该只有一个解决方案)正在进行cc_multi调用,这将有多个解决方案(我们只是想知道什么其中一个是).
那么这究竟是如何有用的呢?好吧,你可以将main(以及它调用的其他东西,如果它有用)声明为cc_multi,并且它们可以毫无问题地调用try.这意味着整个程序理论上有多个"解决方案",但运行它将产生一个解决方案.这允许您编写一个程序,当某个程序在某些时候发生内存不足时,其行为会有所不同.但它并没有破坏声明性语义,因为它可以用更多可用内存计算的"真实"结果仍然在解决方案集中(就像程序实际执行时仍然在解决方案集中的内存不足异常一样)计算一个值,只是我们只得到一个任意的解决方案.
重要的是det(只有一个解决方案)的处理方式不同cc_multi(有多个解决方案,但您只能有一个解决方案).与在Haskell中捕获异常的原因类似,异常捕获不允许在非"已提交选择"上下文中发生,或者您可以根据来自现实世界的信息生成不同的解决方案集来生成不同的解决方案.可以访问.该cc_multi的决定try使我们能够编写程序,就好像它们产生无限的解决方案集(几乎充满不太可能例外的轻微变种),以及阻止我们编写的实际需要,从设定的多个解决方案的程序.[3]
[1]除非评估它首先遇到不确定性错误.现实生活是一种痛苦.
[2]仅仅鼓励程序员在不强制执行的情况下使用纯度的语言(例如Scala)往往只是让你在任何你想要的地方捕获异常,就像它们允许你在任何你想要的地方进行I/O一样.
[3]请注意,"承诺选择"概念并不是Mercury处理纯I/O的方式.为此,Mercury使用独特类型,这与"承诺选择"决定论类正交.
C. *_*ann 14
delnan在评论中提到的文章,以及前一个问题的答案,肯定提供了足够的理由来仅仅捕获例外IO.
但是,我也可以看到为什么观察评价顺序或打破单调性等原因在直观层面上可能没有说服力; 很难想象如何在绝大多数代码中造成很大的伤害.因此,可能有助于回忆异常处理是明显非本地变种的控制流结构,并且能够捕获纯代码中的异常将允许(错误地)将它们用于该目的.
请允许我准确说明这需要什么样的恐怖.
首先,我们定义要使用的异常类型,并且catch可以在纯代码中使用它的版本:
newtype Exit a = Exit { getExit :: a } deriving (Typeable)
instance Show (Exit a) where show _ = "EXIT"
instance (Typeable a) => Exception (Exit a)
unsafeCatch :: (Exception e) => a -> (e -> a) -> a
unsafeCatch x f = unsafePerformIO $ catch (seq x $ return x) (return . f)
Run Code Online (Sandbox Code Playgroud)
这将让我们抛出任何Typeable价值,然后在没有任何干预表达的同意的情况下从一些外部范围中捕获它.例如,我们可以隐藏一个Exit抛出内部的东西,我们传递给一个更高阶的函数来逃避它的评估产生的一些中间值.精明的读者可能已经想到了现在的发展方向:
callCC :: (Typeable a) => ((a -> b) -> a) -> a
callCC f = unsafeCatch (f (throw . Exit)) (\(Exit e) -> e)
Run Code Online (Sandbox Code Playgroud)
是的,这实际上是有效的,需要注意的是,只要整个表达式,它就需要使用延续来进行评估.如果你试试这个,请记住这一点,或者只是使用deepseq来自轨道的nuking更多你的速度.
看吧:
-- This will clearly never terminate, no matter what k is
foo k = fix (\f x -> if x > 100 then f (k x) else f (x + 1)) 0
Run Code Online (Sandbox Code Playgroud)
但:
?x. x ? callCC foo
101
Run Code Online (Sandbox Code Playgroud)
逃离内部map:
seqs :: [a] -> [a]
seqs xs = foldr (\h t -> h `seq` t `seq` (h:t)) [] xs
bar n k = map (\x -> if x > 10 then k [x] else x) [0..n]
Run Code Online (Sandbox Code Playgroud)
请注意强制评估的必要性.
?x. x ? callCC (seqs . bar 9)
[0,1,2,3,4,5,6,7,8,9]
?x. x ? callCC (seqs . bar 11)
[11]
Run Code Online (Sandbox Code Playgroud)
...啊.
现在,让我们再也不谈这个了.
| 归档时间: |
|
| 查看次数: |
3526 次 |
| 最近记录: |