任一类型基础知识
这两种类型都来自函数式语言,其中异常(理所当然地)被视为副作用,因此不适合传递domain错误。请注意不同类型错误之间的区别:其中一些属于领域,另一些则不属于。例如。空引用异常或索引越界与域无关 - 它们更表明存在缺陷。
Either 被定义为具有两个分支的泛型类型 - 成功和失败:Either<TResult, TError>
。它可以以两种形式出现,其中包含一个对象TResult
,或者它包含一个对象的地方TError
. It cannot同时出现在两种状态中,或者都不出现在任何状态中。因此,如果拥有 Either 实例,它要么包含成功生成的结果,要么包含错误对象。
要么和例外
在异常代表对域很重要的事件的情况下,任一类型都会替换异常。但它并不能取代其他场景中的异常。
关于异常的故事很长,从不需要的副作用到简单的泄漏抽象。顺便说一下,泄漏抽象是使用throws
随着时间的推移,关键字在 Java 语言中逐渐消失。
两者之一和副作用
当涉及到副作用时,尤其是与不可变类型结合时,它同样有趣。在任何语言中,无论是函数式语言、OOP 语言还是混合语言(包括 C#、Java、Python),程序员在以下情况下都会采取特定的行为:know某种类型是不可变的。一方面,他们有时倾向于cache结果——完全正确! - 这有助于他们避免以后进行昂贵的调用,例如涉及网络调用甚至数据库的操作。
缓存也可以是微妙的,例如在操作结束之前使用内存中的对象几次。现在,如果不可变类型具有用于域错误结果的单独通道,那么它们将达不到缓存的目的。我们拥有的对象会多次有用,还是应该在每次需要其结果时调用生成函数?这是一个棘手的问题,无知有时会导致代码缺陷。
功能性任一类型实现
这就是任一类型可以提供帮助的地方。我们可以忽略它的内部复杂性,因为它是一个库类型,而只关注它的API。最小任一类型允许:
- 将结果映射到不同的结果或不同类型的结果 - 对于链接快乐路径转换很有用
- 处理错误,有效地将失败转化为成功 - 在顶层很有用,例如当将成功和失败表示为 HTTP 响应时
- 将一个错误转换为另一个错误 - 在传递层边界时很有用(一层中的一组域错误需要转换为另一层的一组域错误)
使用 Either 的最明显的好处是返回它的函数将显式地声明它们返回结果的两个通道。而且,结果将变得稳定,这意味着我们可以在需要时自由缓存它们。另一方面,仅对 Either 类型进行绑定操作有助于避免代码其余部分的污染。一方面,函数永远不会收到 Either。它们将分为对常规对象进行操作的对象(包含在 Either 的 Success 变体中),或对域错误对象进行操作的对象(包含在 Either 的 Failed 变体中)。正是对 Either 的绑定操作选择了将有效调用哪个函数。考虑这个例子:
var response = ReadUser(input) // returns Either<User, Error>
.Map(FindProduct) // returns Either<Product, Error>
.Map(ReadTechnicalDetails) // returns Either<ProductDetails, Error>
.Map(View) // returns Either<HttpResponse, Error>
.Handle(ErrorView); // returns HttpResponse in either case
使用的所有方法的签名都是直接的,并且它们都不会收到 Either 类型。那些方法can检测到错误,允许返回 Either。那些不这样做的,只会返回一个简单的结果。
Either<User, Error> ReadUser(input);
Product FindProduct(User);
Either<ProductDetails, Error> ReadTechnicalDetails(Product);
HttpResponse View(Product);
HttpResponse ErrorView(Product);
所有这些不同的方法都可以绑定到 Either,它将选择是有效地调用它们,还是继续使用它已经包含的内容。基本上,如果调用失败,则映射操作将通过,如果调用成功,则调用该操作。
这就是让我们只编写快乐路径的原则,并在可能的时候处理错误。在大多数情况下,在到达最顶层之前不可能一直处理错误。应用程序通常会通过将错误转换为错误响应来“处理”错误。这种情况正是 Either 类型发挥作用的地方,因为没有其他代码会注意到需要处理错误。
实践中的任一类型
在某些情况下,例如表单验证,需要沿路径收集多个错误。对于这种情况,任一类型都将包含列表,而不仅仅是错误。之前提出的 Either.Map 函数在这种情况下也足够了,只需进行修改即可。常见的Either<Result, Error>.Map(f)
不打电话f
处于失败状态。但Either<Result, List<Error>>.Map(f)
,其中 f 返回Either<Result, Error>
还是会选择打电话f
,仅查看它是否返回错误并将该错误附加到当前列表。
经过这样的分析,很明显,Either 类型代表了一种编程原则,如果你喜欢的话,可以说是一种模式,而不是一种解决方案。如果任何应用程序有一些特定的需求,并且 Either 满足这些需求,那么实现归结为选择适当的绑定,然后应用它们通过 Either 对象来瞄准对象。使用 Either 进行编程变得声明式。呼叫者有责任declare哪些函数适用于正向和负向场景,Either 对象将决定在运行时是否调用函数以及调用哪个函数。
简单的例子
考虑计算算术表达式的问题。通过计算函数对节点进行深入评估,该函数返回Either<Value, ArithmeticError>
。错误如上溢、下溢、被零除等 - 典型的域错误。实现计算器就很简单了:定义节点,可以是普通值或运算,然后实现一些Evaluate
他们每个人的功能。
// Plain value node
class Value : Node
{
private int content;
...
Either<int, Error> Evaluate() => this.content;
}
// Division node
class Division : Node
{
private Node left;
private Node right;
...
public Either<Value, ArithmeticError> Evaluate() =>
this.left.Map(value => this.Evaluate(value));
private Either<Value, ArithmeticError> Evaluate(int leftValue) =>
this.right.Map(rightValue => rightValue == 0
? Either.Fail(new DivideByZero())
: Either.Success(new Value(leftValue / rightValue));
}
...
// Consuming code
Node expression = ...;
string report = expression.Evaluate()
.Map(result => $"Result = {result}")
.Handle(error => $"ERROR: {error}");
Console.WriteLine(report);
此示例演示了求值如何导致在任意点弹出算术错误,而系统中的所有节点都会忽略它。节点只会评估它们的快乐路径,或者自己生成错误。只有在 UI 上才会第一次考虑错误,当某物需要显示给用户。
复杂的例子
在更复杂的算术评估器中,人们可能希望看到所有错误,而不仅仅是一个错误。该问题需要对至少两个帐户进行自定义:(1) Either 必须包含错误列表,(2) 必须添加新 API 以组合两个 Either 实例。
public Either<int, ArithErrorList> Combine(
Either<int, ArithErrorList> a,
Either<int, ArithErrorList> b,
Func<int, int, Either<int, ArithErrorList>> map) =>
a.Map(aValue => Combine(aValue, b, map);
private Either<int, ArithErrorList> Combine(
int aValue,
Either<int, ArithErrorList> b,
Func<int, int, Either<int, ArithErrorList>> map) =>
b.Map(bValue => map(aValue, bValue)); // retains b error list otherwise
private Either<int, ArithErrorList> Combine(
ArithErrorList aError,
Either<int, ArithErrorList> b,
Func<int, int, Either<int, ArithErrorList>> map) =>
b.MapError(bError => aError.Concat(bError))
.Map(_ => bError); // Either concatenate both errors, or just keep b error
...
// Operation implementation
class Divide : Node
{
private Node left;
private Node right;
...
public Either<int, AirthErrorList> Evaluate() =>
helper.Combine(left.Evaluate(), right.Evaluate(), this.Evaluate);
private Either<int, ArithErrorList> Evaluate(int a, int b) =>
b == 0 ? (ArithErrorList)new DivideByZero() : a / b;
}
在本次实施中,公众Combine
method 是入口点,可以连接两个 Either 实例的错误(如果两个都失败),保留一个错误列表(如果只有一个失败),或者调用映射函数(如果两个都成功)。请注意,即使最后一个场景(两个 Either 对象均为 Success)最终也可能产生 Fail 结果!
实施者注意事项
重要的是要注意Combine
方法是库代码。一般规则是,必须对使用代码隐藏神秘、复杂的转换。消费者只会看到简单明了的 API。
在这方面,Combine
方法可以是附加的扩展方法,例如附加到Either<TResult, List<TError>>
or Either<TReuslt, ImmutableList<TError>>
类型,以便在出现错误的情况下它变得可用(不引人注目!)can被合并。在所有其他情况下,当错误类型不是列表时,Combine
方法将不可用。