关于本系列
本系列旨在将您的观点重新定位为实用的心态,帮助您以新的方式看待常见问题并找到改善日常编码的方式。 它探讨了函数式编程的概念,允许在Java语言中进行函数式编程的框架,在JVM上运行的函数式编程语言以及语言设计的一些未来方向。 该系列面向那些了解Java及其抽象如何工作,但很少或没有使用功能语言经验的开发人员。
当您调查诸如函数式编程之类的深层主题时,有时会出现迷人的分支。 在上一期中 ,我继续我的迷你剧,以一种实用的方式重新思考传统的“四人一组”设计模式。 在下一部分中,我将通过讨论Scala样式模式匹配来继续该主题,但是首先,我需要通过一个名为Either的概念来建立一些背景知识。 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类的左侧包含一个异常(如果有),而右侧包含结果。
解析罗马数字
我有一个名为RomanNumeral的类(我将RomanNumeral实现的对象留给读者想象)和一个名为RomanNumeralParser的类,该类调用RomanNumeral类。 清单4中显示了parseNumber()方法和说明性测试:
清单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的左侧,或者将结果放在其右侧 。 两种情况都在单元测试中显示。
与传递Map相比,这是一个很大的改进。 我保留类型安全性(请注意,我可以根据自己的喜好指定异常); 通过泛型在方法声明中的错误是显而易见的; 而我的结果又返回了一个额外的间接级别,从Either解压了结果(无论是异常还是答案)。 间接的额外级别使得懒惰 。
延迟解析和函数式Java
Either类出现在许多函数算法中,并且在函数世界中非常普遍,以至于Functional Java框架(请参阅参考资料 )包含Either的实现,该实现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的左侧拆包,否则不会创建异常(及其昂贵的生成堆栈跟踪)。 因此,异常是惰性的,可让您推迟执行异常的构造函数。
提供默认值
懒惰并不是使用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的右边 。
包装异常
我还可以使用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的左边 ; 否则,我将结果返回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上使用一种方法来允许我将两个罗马数字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 留下了异常。 接下来,我必须解压缩实际的整数值,然后执行其他验证条件。 罗马数字没有零的概念,因此我制定了禁止除以1的规则:如果分母为1,则打包例外并将其放在右边的左边 。
换句话说,我有三个按类型划分的插槽: NumberFormatException , ArithmeticException和Double 。 第一个Either的左侧保留潜在的NumberFormatException ,而其右边保留另一个Either 。 第二个Either的左侧包含一个潜在的ArithmeticException ,其右侧包含有效载荷(即结果)。 因此,要获取实际答案,我必须遍历result.right().value().right().value().doubleValue() ! 显然,这种方法的实用性很快就崩溃了,但是它确实提供了一种类型安全的方式来将异常嵌套为类签名的一部分。
Option类别
在下一部分中,我都会用它来构建树形数据结构, Either是一个方便的概念。 在Scala中类似的称为Option类(在Functional Java中复制)提供了一种更简单的异常情况: none (表示无合法值)或some (表示成功返回)。 清单10中展示了该Option :
清单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都是monads-表示计算的特殊数据结构,并且在功能语言中大量使用。 在下一部分中,我将探讨与Either相关的单子概念,并说明如何在孤立的情况下实现Scala样式的模式匹配。
结论
当您学习新的范例时,您需要重新考虑所有熟悉的解决问题的方法。 函数式编程使用不同的惯用法来报告错误情况,大多数情况可以使用某些公认的复杂语法在Java中复制。
在下一部分中,我将展示如何使用最低的Either来构建树。
翻译自: https://www.ibm.com/developerworks/java/library/j-ft13/index.html
相关资源:微信小程序源码-合集6.rar