如何利用 Either 和 Option 进行函数式错误处理

前言

我将讨论 Scala 风格的模式匹配,但首先我需要通过 Either 概念建立一些背景知识。Either 的其中一个用法是函数式风格的错误处理,我会在本期文章中对其进行介绍。

在 Java 中,错误的处理在传统上由异常以及创建和传播异常的语言支持进行。但是,如果不存在结构化异常处理又如何呢?许多函数式语言不支持异常范式,所以它们必须找到表达错误条件的替代方式。在本文中,我将演示 Java 中类型安全的错误处理机制,该机制绕过正常的异常传播机制(并通过 Functional Java 框架的一些示例协助说明)。

函数式错误处理

如果您想在 Java 中不使用异常来处理错误,最根本的障碍是语言的限制,因为方法只能返回单个值。但是,当然,方法可以 返回单个 Object(或子类)引用,其中可包含多个值。那么,我可以使用一个 Map 来启用多个返回值。请看看清单 1 中的 divide() 方法:

清单 1. 使用 Map 处理多个返回值

public static Map<String, Object> divide(int x, int y) {
Map<String, Object> result = new HashMap<String, Object>();
if (y == 0)
result.put("exception", new Exception("div by zero"));
else
result.put("answer", (double) x / y);
return result;
}

在 清单 1 中,我创建了一个 Map,以 String 为键,并以 Object 为值。在 divide() 方法中,我输出 exception 来表示失败,或者输出 answer 来表示成功。清单 2 中对两种模式都进行了测试:

清单 2. 使用 Map 测试成功与失败

@Test
public void maps_success() {
Map<String, Object> result = RomanNumeralParser.divide(4, 2);
assertEquals(2.0, (Double) result.get("answer"), 0.1);
}
@Test
public void maps_failure() {
Map<String, Object> result = RomanNumeralParser.divide(4, 0);
assertEquals("div by zero", ((Exception) result.get("exception")).getMessage());
}

在 清单 2 中,maps_success 测试验证在返回的 Map 中是否存在正确的条目。maps_failure 测试检查异常情况。

这种方法有一些明显的问题。首先,Map 中的结果无论如何都不是类型安全的,它禁用了编译器捕获特定错误的能力。键的枚举可以略微改善这种情况,但效果不大。其次,该方法调用器并不知道方法调用是否成功,这加重了调用程序的负担,它要检查可能结果的词典。第三,没有什么能阻止这两个键都有值,这使得结果模棱两可。

我需要的是一种让我能够以类型安全的方式返回两个(或多个)值的机制。

Either 类

返回两个不同值的需求经常出现在函数式语言中,用来模拟这种行为的一个常用数据结构是 Either 类。在 Java 中,我可以使用泛型创建一个简单的 Either 类,如清单 3 所示:

清单 3. 通过 Either 类返回两个(类型安全的)值

public class Either<A,B> {
private A left = null;
private B right = null;
private Either(A a,B b) {
left = a;
right = b;
}
public static <A,B> Either<A,B> left(A a) {
return new Either<A,B>(a,null);
}
public A left() {
return left;
}
public boolean isLeft() {
return left != null;
}
public boolean isRight() {
return right != null;
}
public B right() {
return right;
}
public static <A,B> Either<A,B> right(B b) {
return new Either<A,B>(null,b);
}
public void fold(F<A> leftOption, F<B> rightOption) {
if(right == null)
leftOption.f(left);
else
rightOption.f(right);
}
}

在 清单 3中,Either 旨在保存一个 left 或 right 值(但从来都不会同时保存这两个值)。该数据结构被称为不相交并集。一些基于 C 的语言包含 union 数据类型,它可以保存含若干种不同类型的一个实例。不相交并集的槽可以保存两种类型,但只保存其中一种类型的一个实例。Either 类有一个 private 构造函数,使构造成为静态方法 left(A a) 或 right(B b) 的责任。在类中的其他方法是辅助程序,负责检索和调研类的成员。

利用 Either,我可以编写代码来返回异常或 一个合法结果(但从来都不会同时返回两种结果),同时保持类型安全。常见的函数式约定是 Either 类的 left 包含异常(如有),而 right 包含结果。

解析罗马数字

我有一个名为 RomanNumeral 的类(我将其实现​​留给读者去想象)和一个名为 RomanNumeralParser 的类,该类调用 RomanNumeral 类。parseNumber() 方法和说明性测试如清单 4 所示:

清单 4. 解析罗马数字

public static Either<Exception, Integer> parseNumber(String s) {
if (! s.matches("[IVXLXCDM]+"))
return Either.left(new Exception("Invalid Roman numeral"));
else
return Either.right(new RomanNumeral(s).toInt());
}
@Test
public void parsing_success() {
Either<Exception, Integer> result = RomanNumeralParser.parseNumber("XLII");
assertEquals(Integer.valueOf(42), result.right());
}
@Test
public void parsing_failure() {
Either<Exception, Integer> result = RomanNumeralParser.parseNumber("FOO");
assertEquals(INVALID_ROMAN_NUMERAL, result.left().getMessage());
}

在 清单 4 中,parseNumber() 方法执行一个验证(用于显示错误),将错误条件放置在 Either 的 left 中,或将结果放在它的 right中。单元测试中显示了这两种情况。

比起到处传递 Map,这是一个很大的改进。我保持类型安全(请注意,我可以按自己喜欢使异常尽量具体);在通过泛型的方法声明中,错误是明显的;返回的结果带有一个额外的间接级别,可以解压 Either 的结果(是异常还是答案)。额外的间接级别支持惰性。

惰性解析和 Functional Java

Either 类出现在许多函数式算法中,并且在函数式世界中如此之常见,以致 Functional Java 框架(参阅 参考资料)也包含了一个 Either 实现,该实现将在 清单 3 和 清单 4 的示例中使用。但它的目的就是与其他 Functional Java 构造配合使用。因此,我可以结合使用 Either 和 Functional Java 的 P1 类来创建惰性 错误评估。惰性表达式是一个按需执行的表达式(参阅 参考资料)。

在 Functional Java 中,P1 类是一个简单的包装器,包括名为 _1() 的方法,该方法不带任何参数。(其他变体:P2 和 P3 等,包含多种方法。)P1 在 Functional Java 中用于传递一个代码块,而不执行它,使您能够在自己选择的上下文中执行代码。

在 Java 中,只要您 throw 一个异常,异常就会被实例化。通过返回一个惰性评估的方法,我可以将异常创建推迟到以后。请看看清单 5 中的示例及相关测试:

清单 5. 使用 Functional Java 创建一个惰性解析器

public static P1<Either<Exception, Integer>> parseNumberLazy(final String s) {
if (! s.matches("[IVXLXCDM]+"))
return new P1<Either<Exception, Integer>>() {
public Either<Exception, Integer> _1() {
return Either.left(new Exception("Invalid Roman numeral"));
}
};
else
return new P1<Either<Exception, Integer>>() {
public Either<Exception, Integer> _1() {
return Either.right(new RomanNumeral(s).toInt());
}
};
}
@Test
public void parse_lazy() {
P1<Either<Exception, Integer>> result = FjRomanNumeralParser.parseNumberLazy("XLII");
assertEquals((long) 42, (long) result._1().right().value());
}
@Test
public void parse_lazy_exception() {
P1<Either<Exception, Integer>> result = FjRomanNumeralParser.parseNumberLazy("FOO");
assertTrue(result._1().isLeft());
assertEquals(INVALID_ROMAN_NUMERAL, result._1().left().value().getMessage());
}

清单 5 中的代码与 清单 4 中的类似,但多了一个 P1 包装器。在 parse_lazy 测试中,我必须通过在结果上调用 _1() 来解压结果,该方法返回 Either 的 right,从该返回值中,我可以检索值。在 parse_lazy_exception 测试中,我可以检查是否存在一个 left,并且我可以解压异常,以辨别它的消息。

在您调用 _1() 解压 Either 的 left 之前,异常(连同其生成成本昂贵的堆栈跟踪)不会被创建。因此,异常是惰性的,让您推迟异常的构造程序的执行。

提供默认值

惰性不是使用 Either 进行错误处理的惟一好处。另一个好处是,您可以提供默认值。请看清单 6 中的代码:

清单 6. 提供合理的默认返回值

public static Either<Exception, Integer> parseNumberDefaults(final String s) {
if (! s.matches("[IVXLXCDM]+"))
return Either.left(new Exception("Invalid Roman numeral"));
else {
int number = new RomanNumeral(s).toInt();
return Either.right(new RomanNumeral(number >= MAX ? MAX : number).toInt());
}
}
@Test
public void parse_defaults_normal() {
Either<Exception, Integer> result = FjRomanNumeralParser.parseNumberDefaults("XLII");
assertEquals((long) 42, (long) result.right().value());
}
@Test
public void parse_defaults_triggered() {
Either<Exception, Integer> result = FjRomanNumeralParser.parseNumberDefaults("MM");
assertEquals((long) 1000, (long) result.right().value());
}

在 清单 6 中,假设我不接受任何大于 MAX 的罗马数字,任何企图大于该值的数字都将被默认设置为 MAX。parseNumberDefaults() 方法确保默认值被放置在 Either 的 right 中。

包装异常

我也可以使用 Either 来包装异常,将结构化异常处理转换成函数式,如清单 7 所示:

清单 7. 捕获其他人的异常

public static Either<Exception, Integer> divide(int x, int y) {
try {
return Either.right(x / y);
} catch (Exception e) {
return Either.left(e);
}
}
@Test
public void catching_other_people_exceptions() {
Either<Exception, Integer> result = FjRomanNumeralParser.divide(4, 2);
assertEquals((long) 2, (long) result.right().value());
Either<Exception, Integer> failure = FjRomanNumeralParser.divide(4, 0);
assertEquals("/ by zero", failure.left().value().getMessage());
}

在 清单 7 中,我尝试除法,这可能引发一个 ArithmeticException。如果发生异常,我将它包装在 Either 的 left 中;否则我在 right 中返回结果。使用 Either 使您可以将传统的异常(包括检查的异常)转换成更偏向于函数式的风格。

当然,您也可以惰性包装从被调用的方法抛出的异常,如清单 8 所示:

清单 8. 惰性捕获异常

public static P1<Either<Exception, Integer>> divideLazily(final int x, final int y) {
return new P1<Either<Exception, Integer>>() {
public Either<Exception, Integer> _1() {
try {
return Either.right(x / y);
} catch (Exception e) {
return Either.left(e);
}
}
};
}
@Test
public void lazily_catching_other_people_exceptions() {
P1<Either<Exception, Integer>> result = FjRomanNumeralParser.divideLazily(4, 2);
assertEquals((long) 2, (long) result._1().right().value());
P1<Either<Exception, Integer>> failure = FjRomanNumeralParser.divideLazily(4, 0);
assertEquals("/ by zero", failure._1().left().value().getMessage());
}

嵌套异常

Java 异常有一个不错的特性,它能够将若干种不同的潜在异常类型声明为方法签名的一部分。尽管语法越来越复杂,但 Either 也可以做到这一点。例如,如果我需要 RomanNumeralParser 上的一个方法允许我对两个罗马数字执行除法,但我需要返回两种不同的可能异常情况,那么是解析错误还是除法错误?使用标准的 Java 泛型,我可以嵌套异常,如清单 9 所示:

清单 9. 嵌套异常

public static Either<NumberFormatException, Either<ArithmeticException, Double>>
divideRoman(final String x, final String y) {
Either<Exception, Integer> possibleX = parseNumber(x);
Either<Exception, Integer> possibleY = parseNumber(y);
if (possibleX.isLeft() || possibleY.isLeft())
return Either.left(new NumberFormatException("invalid parameter"));
int intY = possibleY.right().value().intValue();
Either<ArithmeticException, Double> errorForY =
Either.left(new ArithmeticException("div by 1"));
if (intY == 1)
return Either.right((fj.data.Either<ArithmeticException, Double>) errorForY);
int intX = possibleX.right().value().intValue();
Either<ArithmeticException, Double> result =
Either.right(new Double((double) intX) / intY);
return Either.right(result);
}
@Test
public void test_divide_romans_success() {
fj.data.Either<NumberFormatException, Either<ArithmeticException, Double>> result =
FjRomanNumeralParser.divideRoman("IV", "II");
assertEquals(2.0,result.right().value().right().value().doubleValue(), 0.1);
}
@Test
public void test_divide_romans_number_format_error() {
Either<NumberFormatException, Either<ArithmeticException, Double>> result =
FjRomanNumeralParser.divideRoman("IVooo", "II");
assertEquals("invalid parameter", result.left().value().getMessage());
}
@Test
public void test_divide_romans_arthmetic_exception() {
Either<NumberFormatException, Either<ArithmeticException, Double>> result =
FjRomanNumeralParser.divideRoman("IV", "I");
assertEquals("div by 1", result.right().value().left().value().getMessage());
}

在 清单 9 中,divideRoman() 方法首先解压从 清单 4 的原始 parseNumber() 方法返回的 Either。如果在这两次数字转换的任一次中发生一个异常,Either left 与异常一同返回。接下来,我必须解压实际的整数值,然后执行其他验证标准。罗马数字没有零的概念,所以我制定了一个规则,不允许除数为 1:如果分母是 1,我打包我的异常,并放置在 right 的 left 中。

换句话说,我有三个槽,按类型划分:NumberFormatException、ArithmeticException 和 Double。第一个 Either 的 left 保存潜在的 NumberFormatException,它的 right 保存另一个 Either。第二个 Either 的 left 包含一个潜在的 ArithmeticException,它的 right 包含有效载荷,即结果。因此,为了得到实际的答案,我必须遍历 result.right().value().right().value().doubleValue()!显然,这种方法的实用性迅速瓦解,但它确实提供了一个类型安全的方式,将异常嵌套为类签名的一部分。

Option 类

Either 是一个方便的概念,在下期文章中,我将使用这个概念构建树形数据结构。Scala 中有一个名为 Option 的类与之类似,该类在 Functional Java 中被复制,提供了一个更简单的异常情况:none 表示不合法的值,some 表示成功返回。Option 如清单 10 所示:

清单 10. 使用 Option

public static Option<Double> divide(double x, double y) {
if (y == 0)
return Option.none();
return Option.some(x / y);
}
@Test
public void option_test_success() {
Option result = FjRomanNumeralParser.divide(4.0, 2);
assertEquals(2.0, (Double) result.some(), 0.1);
}
@Test
public void option_test_failure() {
Option result = FjRomanNumeralParser.divide(4.0, 0);
assertEquals(Option.none(), result);
}

如 清单 10 所示,Option 包含 none() 或 some(),类似于 Either 中的 left 和 right,但特定于可能没有合法返回值的方法。

Functional Java 中的 Either 和 Option 都是单体,表示计算 的特殊数据结构,在函数式语言中大量使用。在下一期中,我将探讨有关 Either 的单体概念,并在不同的示例中演示它如何支持 Scala 风格的模式匹配。

结束语

当您学习一种新范式时,您需要重新考虑所有熟悉的问题解决方式。函数式编程使用不同的习惯用语来报告错误条件,其中大部分可以在 Java 中复制,不可否认,也有一些令人费解的语法。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 深入了解java 8的函数式编程

    前言 关于"Java 8为Java带来了函数式编程"已经有了很多讨论,但这句话的真正意义是什么? 本文将讨论函数式,它对一种语言或编程方式意味着什么.在回答"Java 8的函数式编程怎么样"之前,我们先看看Java的演变,特别是它的类型系统,我们将看到Java 8的新特性,特别是Lambda表达式如何改变Java的风景,并提供函数式编程风格的主要优势. 函数式编程语言是什么? 函数式编程语言的核心是它以处理数据的方式处理代码.这意味着函数应该是第一等级(First-

  • java8新特性之Optional的深入解析

    前言 最近脑袋发热追着java8源码看的很起劲,还有了执念,罪过. 本文以jdk1.8.0_111源码为例 public final class Optional<T> {} Optional是一个为了解决NullPointerException设计而生可以包含对象也可以包含空的容器对象.封装了很多对空处理的方法也增加了filter.map这样的检索利器,其中函数式编程会有种炫酷到爆的感觉. 基础测试用例对象: public class Java8OptionalTest { List<

  • 简单了解java函数式编码结构及优势

    前言 当垃圾回收成为主流时,它消除了所有类别的难以调试的问题,使运行时能够为开发人员管理复杂的.容易出错的进程.函数式编程旨在为您编写的算法实现同样的优化,这样您就可以从一个更高的抽象层面开展工作,同时运行时执行复杂的优化. Java 下一代语言并不都占用从命令式到函数式的语言频谱的同一位置,但都展现出函数功能和习语.函数式编程技术有明确定义,但语言有时为相同的函数式概念使用不同的术语,使得我们很难看到相似之处.在本期文章中,我比较了 Scala.Groovy 和 Clojure 的函数式编码风

  • 深入学习java内存化和函数式协同

    前言 所有编程语言都在增加函数特性,因为运行时已变得强大到足够适应性能或内存开销.函数式编程的许多收益之一是,您可将麻烦或容易出错的任务卸载到运行时.另一个收益是将函数特性简洁地组合到您代码中的能力. 在本期文章中,我将探讨 Java 下一代语言中的内存化.然后,通过利用 Clojure 示例,我将展示通过利用函数特性之间的协调作用,如何实现常见问题的一般解决方案. 内存化 内存化 这个词是 Donald Michie(一位英国人工智能研究人员)发明的,用于表示重复的值的函数级缓存.如今,内存化

  • Vue使用axios出现options请求方法

    以下代码如果需要在你本地跑起来: Ⅰ.需要apache服务,并把php代码丢进去指定位置 Ⅱ.将下面http://www.test.com/day05/jiekou.php地址,改成自己指定位置的域名或ip 1.从一段简易代码说起 前端代码 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewpor

  • Java8中Optional的一些常见错误用法总结

    前言 Java 8 引入的 Optional 类型,基本是把它当作 null 值优雅的处理方式.其实也不完全如此,Optional 在语义上更能体现有还是没有值.所以它不是设计来作为 null 的替代品,如果方法返回 null 值表达了二义性,没有结果或是执行中出现异常. 在 Oracle  做  Java 语言工作的  Brian Goetz 在 Stack Overflow 回复 Should Java 8 getters return optional type? 中讲述了引入  Opti

  • 如何利用 Either 和 Option 进行函数式错误处理

    前言 我将讨论 Scala 风格的模式匹配,但首先我需要通过 Either 概念建立一些背景知识.Either 的其中一个用法是函数式风格的错误处理,我会在本期文章中对其进行介绍. 在 Java 中,错误的处理在传统上由异常以及创建和传播异常的语言支持进行.但是,如果不存在结构化异常处理又如何呢?许多函数式语言不支持异常范式,所以它们必须找到表达错误条件的替代方式.在本文中,我将演示 Java 中类型安全的错误处理机制,该机制绕过正常的异常传播机制(并通过 Functional Java 框架的

  • 如何利用Oracle命令解决函数运行错误

    1 问题 自定义了一个 Oracle 函数.编译正常:使用 PL/SQL Developer 的 Test 窗口模式,测试通过.但 Java 直接调用失败:使用 PL/SQL Developer 的 SQL 窗口模式,执行失败. 没有有效的错误提示信息. 2 分析 肯定是函数本身有问题,我们要使用有效的工具来定位出问题. 在 Oracle 函数中,加入异常处理. 异常处理 (EXCEPTION)  可用来处理正常执行过程中未预料的事件.如果 PL/SQL 程序块产生异常,但没有指定如何处理时 ,

  • Global.asax的Application_Error实现错误记录/错误日志的代码

    利用Global.asax的Application_Error实现错误记录 错误日志 复制代码 代码如下: void Application_Error(object sender, EventArgs e) { // 在出现未处理的错误时运行的代码 Exception ex = Server.GetLastError().GetBaseException(); StringBuilder str = new StringBuilder(); str.Append("\r\n" + D

  • Restful API中的错误处理方法

    简介 随着移动开发和前端开发的崛起,越来越多的 Web 后端应用都倾向于实现 Restful API. Restful API 是一个简单易用的前后端分离方案,它只需要对客户端请求进行处理,然后返回结果即可, 无需考虑页面渲染,一定程度上减轻了后端开发人员的负担. 然而,正是由于 Restful API 不需要考虑页面渲染,导致它不能在页面上展示错误信息. 那就意着当出现错误的时候,它只能通过返回一个错误的响应,来告诉用户和开发者相应的错误信息,提示他们接下来应该怎么办. 本文将讨论 Restf

  • 正则表达式在IOS中的应用及IOS中三种正则表达式的使用与比较

    正则表达式在ios中应用 一.什么是正则表达式 正则表达式,又称正规表示法,是对字符串操作的一种逻辑公式.正则表达式可以检测给定的字符串是否符合我们定义的逻辑,也可以从字符串中获取我们想要的特定部分.它可以迅速地用极简单的方式达到字符串的复杂控制. 二.正则表达式的语法 看一个过滤纯数字的例子 - (BOOL)validateNumber:(NSString *) textString { NSString* number=@"^[0-9]+$"; NSPredicate *numbe

  • FCKeditor的几点修改小结

    在我的文章系统中,打算使用FCKeditor作为在线文本编辑器,在开发过程中,有几个地方需要对编辑器 进行修改,才能满足功能设计. 一.修改默认的上传文件名     FCKeditor上传文件时,默认使用客户端的文件名,遇到重名文件,则自动重新命名,对于中文名称 的文件也是如此,这样对中文支持不好的服务器和浏览器,就容易对中文文件名的编解码出现错误而导致 乱码,因此需要将默认的文件命名方法修改为以当前日期.时间为基本要素的文件名,同时,为了避免重 复,还要用到随机函数.在.NET中,可以用Sys

  • linux shell命令行参数用法详解

    习惯使用linux命令行来管理linux系统,例如: 复制代码 代码如下: $ date 二 11 23 01:34:58 CST 1999  $ 用户登录时,实际进入了shell,它遵循一定的语法将输入的命令加以解释并传给系统.命令行中输入的第一个字必须是一个命令的名字,第二个字是命令的选项或参数,命令行中的每个字必须由空格或TAB隔开,格式如下:  复制代码 代码如下: $ Command Option Arguments 一,选项和参数 选项是包括一个或多个字母的代码,它前面有一个减号(减

  • 简单学习5种处理Vue.js异常的方法

    错误大全 为了测试各种异常处理技巧,我故意触发三种类型的错误. 第一种:引用一个不能存在的变量: <div id="app" v-cloak> Hello, {{name}} </div> 上述代码运行后不会抛出错误,但是在控制台会有[Vue warn]消息. 111111111111111111 你可以在Codepen查看例子完整代码. 第二种:将变量绑定到一个被计算出来的属性,计算的时候会抛出异常. <div id="app" v-

  • mysql 5.7.17 64bit安装配置方法图文教程

    win(10) 64bit 安装 mysql 5.7.17 过程 新版本的64位 mysql ,没有mis安装文件.只有免安装版的ZIP版本. 根据官网的安装说明,和网上的教程,安装启动成功. 整理安装过程,以及过程中可能会遇到几次错误,整理如下. step1:下载mysql5.7.17 打开mysql官方网站下载页面:http://www.mysql.com/downloads/ 个人及小团队使用,选择下载社区版 下载后,解压到自定位置.(若JAVA,习惯路径中无空格).如保存至 D:\Pro

  • Java通过jersey实现客户端图片上传示例

    在上一篇笔记 <SpringMVC实现图片上传>记录了将图片上传到本地的实现,在很多项目中都会有一台专门的文件服务器来保存文件的,这边记录下客户端通过jersey上传图片到文件服务端的实现. 由于要在不同主机上上传文件,所以不能直接通过流的方式来写,需要通过webService来完成,jersey是基于Java的一个轻量级RESTful风格的Web Services框架,它让客户端文件上传变得更简单. 1. maven依赖 spring的一些包以及fileupload和io包这边就不贴出来了.

随机推荐