在所有编程语言中,我们都需要处理无法“失败”的操作:
- 纯函数可能无法产生结果,或者
- 一个不纯净的函数可能无法产生所需的副作用(创建一个新文件,或其他)。
在任何一种情况下,我们都不能盲目地继续其余的计算。 在第一种情况下,函数的结果可能是其他函数的输入。 在第二种情况下,后续操作可能会假定发生了副作用(该文件现在存在,或其他原因)。
因此,很明显,某种操作必须有某种方式可以向调用代码发出失败信号。 编程语言为信令失败提供了两种广泛的机制:
尽管细节有所不同,但大多数现代编程语言都支持这两种机制。 特别是,语言提供了不同程度的类型安全性。
- 在对求和类型或联合类型提供适当支持的语言中,可以使用返回值以非常健壮和类型安全的方式对失败进行建模。
- 在具有某种效果类型的语言中,例如Java样式检查的异常,这些异常本身是类型安全的。
通过类型安全 ,我的意思是,一个可能失败的操作声明了其签名失败的可能性,并且编译器强制立即调用代码以明确处理该失败。
故障类型
那么,语言应该为建模失败提供哪些便利? 返回码或异常? 类型安全与否? 为了部分回答这个问题,让我们从以下“失败”分类开始:
- 某些故障表示立即调用代码不太可能从中恢复的问题。 示例包括事务回滚,网络故障,内存不足或堆栈溢出。
- 某些故障通常是程序逻辑中的错误导致的。 示例包括断言失败,被零除和使用空指针。 这是一类故障,我们希望在编译时尽可能地进行检测,但是没有任何类型系统具有足够的能力来检测所有这些故障。 经过几分钟的思考,您应该能够使自己确信此类问题实际上是第一类的子类:任何计算如何从其逻辑中的错误中有意义地恢复?
- 最后,有些“故障”通常表示可恢复的状况。 例如,可以通过创建文件来从不存在的文件中恢复。 请注意,此类中的失败不一定总是可以恢复的。
根据这种分类,我相对较快地得出以下结论。
处理可恢复的故障
对于“可恢复”条件,故障应为类型安全的。 编译器应该能够验证调用代码是否已明确决定如何处理故障:从故障中恢复或将其转换为不可恢复的故障。 我们希望防止意外发现可恢复的故障。
显然,未经检查的异常(或其他非类型安全的解决方案,例如以Java(其中null
为非类型安全的语言)返回null
不会阻止这种情况,并且会导致故障情况不被注意,从而导致错误。
表示可恢复故障的最方便,最优雅,最有效的方法是联合类型的返回值。 例如,如果我有一个解析JSON的函数,并且可能因非法输入而失败,则可以使用具有以下签名的函数:
JsonObject|ParseError parseJson() => ... ;
或者,如果ParseError
似乎没有任何有用的信息,我可以只使用Null
:
JsonObject? parseJson() => ... ;
或者,在某些高级情况下,可以使用求和(枚举)返回类型。
interface ParseResult of ParseSuccessful | ParseError {}
class ParseSuccessful(shared JsonObject result) satisfies ParseResult {}
class ParseError(shared String message) satisfies ParseResult {}
ParseResult parseJson() => ... ;
但是,在锡兰通常不需要这样做。
处理不可恢复的故障
现在,对于“不可恢复”的情况,故障应该是未类型化(未经检查)的异常。 对于无法恢复的失败,我们不应该因为担心调用代码可能无法做任何有用的事情而污染它。 我们希望故障能够快速透明地传播到某些集中的,通用的,基础架构级别的错误处理。
请注意,由于未经检查的异常不会出现在操作的签名中,因此调用方不会收到任何可能发生的“合理警告”。 它们代表类型系统中的一种设计“洞”。
有疑问时
但是,等等,您可能在想,我不是在这里乞求一个大问题吗?
不能完全归入“可恢复”或“不可恢复”的失败呢?
那里是否没有一个巨大的灰色区域,充满了有时可以通过立即调用的代码恢复的故障?
确实有。 我想说,根据经验, 将这些故障视为可恢复的 。
考虑上面的parseJson()
函数。 给定JSON文本中的语法错误很可能是我们程序中的错误的结果,但是至关重要的是, 这不是parseJson()
本身的错误。 知道是程序错误还是其他原因的代码是调用代码,而不是parseJson()
函数。
调用代码将可恢复的故障转换为不可恢复的故障总是很容易的。 例如:
assert (is JsonObject result = parseJson(json));
//result must be a JsonObject here
要么:
value result = parseJson(json);
if (is ParseError result) {
throw AssertionError(result.message);
}
//result must be a JsonObject here
也就是说,当有疑问时,我们使调用代码明确记录其假设。
从另一种角度来看,我们在类型安全性方面犯了错误,因为有太多未经检查的异常开始破坏静态类型系统的整个价值。
此外,很有可能调用代码比调用代码更适合产生错误,并提供更有意义的信息(尽管我没有在上面的代码片段中对此进行说明)。
要考虑的三件事
我答应为我的原始问题提供“部分”答案,因为仍然有几个问题我不确定我是否完全回答了这个问题,而且锡兰社区也对这些问题进行了辩论。
是否过度使用了AssertionError
?
首先, AssertionError
合法使用是什么类型的故障? 每个AssertionError
应该代表程序中的错误吗? 库在遇到考虑滥用其API的情况时抛出AssertionError
是否合理? 从AssertionError
恢复通用的异常处理代码是否可以接受,还是应该将AssertionError
视为致命的?
我的回答是是,是和是。 但这也许意味着遵循Java将AssertionError
为Error
而不是普通的Exception
是Error
的。 (这引起了有关Error
角色的更大争论。)
Null
是否被过度使用?
其次, Null
类是一种诱人的便捷方式,用于表示返回值的函数失败。 但是我们过度使用了吗? 将Map<Key,Item>.get()
的返回类型设为Item|NoItem<Key>
而不是更通用的Item?
会更好Item?
,即Item|Null
?
也许。 从某种意义上说,返回null
就像抛出Exception
:有点太通用了。 但是,由于必须由立即调用的代码来处理null返回值,因此最好知道该null
实例表示什么,因此它的危害不如从源头处理的泛型Exception
那样有害。
无论您是否同意,仍然最好避免操作可能因多种不同的故障情况而导致null
操作。 我过去曾违反此规则,将来会更加小心。
没有有用的返回值的函数
第三,对于没有有用返回值的函数,也就是说,仅出于副作用而调用的函数(调用代码可以选择忽略表示失败的任何返回值),我们应该错误地抛出异常?
或者,该语言是否应该提供某种方法来强制调用者使用non- void
函数的返回值执行某些操作 ?
锡兰没有(也不会有)检查异常,但是有人可能会认为这是最有用的一种情况。
结论
因此,最终会有一些未解决的问题和灰色区域,但是在我看来,至少我们有一个相当强大的概念框架来研究这些问题。 显然,设施的组合(联合类型以及未检查的异常)是强大的故障处理的强大基础。
翻译自: https://www.javacodegeeks.com/2015/12/modelling-failure-ceylon.html