zoukankan      html  css  js  c++  java
  • 函数式思维: 利用 Either 和 Option 进行函数式错误处理 类型安全的函数式异常

    当您研究函数式编程等深奥学科时,令人着迷的分支偶尔会出现。在 函数式思维:函数设计模式,第 3 部分 中,我在迷你系列中继续以函数的方式重新思考传统的 Gang of Four 设计模式。在下一期文章中,我将回到这一主题,讨论 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 旨在保存一个 leftright 值(但从来都不会同时保存这两个值)。该数据结构被称为不相交并集。一些基于 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() 方法执行一个验证(用于显示错误),将错误条件放置在 Eitherleft 中,或将结果放在它的 right中。单元测试中显示了这两种情况。

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

    惰性解析和 Functional Java

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

    在 Functional Java 中,P1 类是一个简单的包装器,包括名为 _1() 的方法,该方法不带任何参数。(其他变体:P2P3 等,包含多种方法。)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() 来解压结果,该方法返回 Eitherright,从该返回值中,我可以检索值。在 parse_lazy_exception 测试中,我可以检查是否存在一个 left,并且我可以解压异常,以辨别它的消息。

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

    提供默认值

    惰性不是使用 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 的罗马数字,任何企图大于该值的数字都将被默认设置为 MAXparseNumberDefaults() 方法确保默认值被放置在 Eitherright 中。

    包装异常

    我也可以使用 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。如果发生异常,我将它包装在 Eitherleft 中;否则我在 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,我打包我的异常,并放置在 rightleft 中。

    换句话说,我有三个槽,按类型划分:NumberFormatExceptionArithmeticExceptionDouble。第一个 Eitherleft 保存潜在的 NumberFormatException,它的 right 保存另一个 Either。第二个 Eitherleft 包含一个潜在的 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 中的 leftright,但特定于可能没有合法返回值的方法。

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

    回页首

    结束语

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

    在下一期中,我将深入探讨单体,讨论这个迷人的概念的一些用法,并展示如何使用低级的 Either 构建树。

  • 相关阅读:
    HTML5结构
    HTML5新增的非主体元素header元素、footer元素、hgroup元素、adress元素
    CF GYM 100703G Game of numbers
    CF GYM 100703I Endeavor for perfection
    CF GYM 100703K Word order
    CF GYM 100703L Many questions
    CF GYM 100703M It's complicate
    HDU 5313 Bipartite Graph
    CF 560e Gerald and Giant Chess
    POJ 2479 Maximum sum
  • 原文地址:https://www.cnblogs.com/shihao/p/2594167.html
Copyright © 2011-2022 走看看