【问题标题】:Does using option type remove need for if statements?使用选项类型是否需要 if 语句?
【发布时间】:2016-07-17 10:13:35
【问题描述】:

我正在阅读 Scala 中的函数式编程,这是它为使用 Option 类型而不是检查 null 提供的两个优点:

  1. 它允许错误以静默方式传播——调用者可能会忘记检查此条件并且不会被编译器警告,这可能会导致后续代码无法正常工作。通常要到代码的后期才能检测到错误。

  2. 除了容易出错之外,它还会在调用站点产生大量样板代码,并带有明确的 if 语句来检查调用者是否收到了“真实”结果。如果您碰巧调用了几个函数,每个函数都使用必须以某种方式检查和聚合的错误代码,那么这个样板文件就会被放大。

对于第 2 点。虽然不必检查 null,但仍然必须检查 Option 类型是否包含 SomeNoneif 检查没有被删除,它只是更加明确,而不是类型是 null 它可能不包含值。这是正确的解释吗?

虽然我引用了一本 Scala 书籍,但 Option 类型在 Java 8 中也可用,所以我认为在 Java 部分也有效。

【问题讨论】:

  • 这似乎是一种观察和半问题的云。不太确定“答案”会是什么样子。你想知道什么?
  • @SethTisue “除了容易出错之外,它还会在调用站点产生大量样板代码,并带有明确的 if 语句来检查调用者是否收到了“真实”结果。”也不必显式检查可选项是否包含值?所以 if 语句仍然存在
  • Option 提供了许多高阶函数,例如 mapflatMapfilter 等等,它们知道如何处理 SomeNone 情况。大多数时候你应该使用这些。第二个最常见的,模式匹配。最不常见,实际使用if

标签: java scala lambda functional-programming optional


【解决方案1】:

这个想法是,与其费心检查您的OptionSome 还是None,您只需使用一系列monadic 操作 将其转换为另一个Option 或最后只需提供一个默认值。想象一下在 java 中是这样的:

 int getUserApartmentNumber(User user) {
    if(user != null) {
       Address address = user.getAddress();
       if(address != null) {
          Building building = address.getBuilding();
          if (building != null) {
             Apartment apartment = building.getApartment();
             if(apartment != null) {
                return apartment.getNumber();
             }
          }
        }   
     }
     return -1;
 }

有很多方法可以用 scala 编写它,每一种都比这更漂亮。例如(假设所有“可空”对象都由Option 表示):

 def getUserApartmentNumber(user: Option[User]) = user
   .flatMap(_.address)
   .flatMap(_.building)
   .flatMap(_.apartment)
   .map(_.getNumber)
   .getOrElse(-1)

【讨论】:

    【解决方案2】:

    Excursion:真正面向对象的布尔值

    Scala 和 Java 都是面向对象的语言。在面向对象的语言中,您永远需要ifs 来处理任何事情!您可以总是用多态消息调度替换条件。 (如果您不熟悉 Smalltalk 术语,请将“消息”替换为“虚拟方法”。)

    消息发送有什么作用?好吧,基本上,它说“if 对象有这个类,then 执行这个方法,if 它有那个类,then 执行那个其他方法”,等等。你有看到?消息发送已经是有条件的! Smalltalk 语言甚至没有条件(它也没有循环),而是使用多态消息分发。

    基本思想是这样的:你有一个用于布尔值的协议 /interface /abstract class,它有一个名为 ifThenElse 的方法,它采用两个代码块(lambdas、函数)作为参数。你有两个具体的实现子类,一个用于实现ifThenElse 的“真”值,这样它就执行它的第一个参数并忽略第二个参数,另一个用于“假”值的具体实现子类,ifThenElse 执行第二个参数并忽略第一个。

    如果你仔细想想,这实际上只是将Replace Conditional With Polymorphism Refactoring 带到了逻辑极端并应用于if/then/else 本身。

    它看起来有点像这样(人为的)Scala 示例:

    sealed abstract trait Buul {
      def apply[T, U <: T, V <: T](thn: => U)(els: => V): T
    }
    
    case object Tru extends Buul {
      override def apply[T, U <: T, V <: T](thn: => U)(els: => V): U = thn
    }
    
    case object Fls extends Buul {
      override def apply[T, U <: T, V <: T](thn: => U)(els: => V): V = els
    }
    
    object BuulExtension {
      import scala.language.implicitConversions
      implicit def boolean2Buul(b: => Boolean) = if (b) Tru else Fls
    }
    
    import BuulExtension._
    
    (2 < 3) { println("2 is less than 3") } { println("2 is greater than 3") }
    // 2 is less than 3
    

    在 Java 中看起来像这样:

    import static java.lang.System.out;
    
    public class Test {
      public static void main(String... args) {
        Buul.from(2 < 3).ifThenElse(
          () -> { out.println("2 is less than 3"); }, 
          () -> { out.println("2 is greater than 3"); }
        );
        // 2 is less than 3
      }
    }
    
    interface Buul {
      void ifThenElse(CodeBlock thn, CodeBlock els);
    
      static Buul from(boolean b) { return b ? new Tru() : new Fls(); };
    
      class Tru implements Buul {
        @Override public void ifThenElse(CodeBlock thn, CodeBlock els) { thn.execute(); };
      }
    
      class Fls implements Buul {
        @Override public void ifThenElse(CodeBlock thn, CodeBlock els) { els.execute(); };
      }
    }
    
    // Somehow, this is missing from java.util.functions
    @FunctionalInterface interface CodeBlock {
      void execute();
    }
    

    andor 可以用类似的方式实现。让我们暂时忽略短路。 true &amp;&amp; something 是什么?好吧,它只取决于something,不是吗?如果something 为假,则结果为假,如果something 为真,则结果为真,或者换句话说:结果始终为something。所以,我们可以在我们的“真正”子类中实现and,就像Boolean and(Boolean other) { return other; }一样。

    这同样适用于false || something

    同样,对于false &amp;&amp; somethingtrue || something,结果始终是第一个操作数,或者换句话说:return this;

    现在,为了实现短路,我们所要做的就是将参数包装在一个函数中,而不是返回参数,而是调用函数并返回其结果。

    以下是 Scala 和 Java 的完整示例:

    sealed abstract trait Buul {
      def apply[T, U <: T, V <: T](thn: => U)(els: => V): T
      def &&&(other: => Buul): Buul
      def |||(other: => Buul): Buul
      def ntt: Buul
    }
    
    case object Tru extends Buul {
      override def apply[T, U <: T, V <: T](thn: => U)(els: => V): U = thn
      override def &&&(other: => Buul) = other
      override def |||(other: => Buul): this.type = this
      override def ntt = Fls
    }
    
    case object Fls extends Buul {
      override def apply[T, U <: T, V <: T](thn: => U)(els: => V): V = els
      override def &&&(other: => Buul): this.type = this
      override def |||(other: => Buul) = other
      override def ntt = Tru
    }
    
    object BuulExtension {
      import scala.language.implicitConversions
      implicit def boolean2Buul(b: => Boolean) = if (b) Tru else Fls
    }
    
    import BuulExtension._
    
    (2 < 3) { println("2 is less than 3") } { println("2 is greater than 3") }
    // 2 is less than 3
    
    import java.util.function.Supplier;
    import static java.lang.System.out;
    
    public class Test {
      public static void main(String... args) {
        Buul.from(2 < 3).ifThenElse(
          () -> { out.println("2 is less than 3"); }, 
          () -> { out.println("2 is greater than 3"); }
        );
        // 2 is less than 3
      }
    }
    
    interface Buul {
      <T, U extends T, V extends T> T ifThenElse(Supplier<U> thn, Supplier<V> els);
      void ifThenElse(CodeBlock thn, CodeBlock els);
      Buul and(Supplier<Buul> other);
      Buul or(Supplier<Buul> other);
      Buul not();
    
      static Buul from(boolean b) { return b ? new Tru() : new Fls(); }
    
      static class Tru implements Buul {
        @Override public <T, U extends T, V extends T> T ifThenElse(Supplier<U> thn, Supplier<V> els) { return thn.get(); }
        @Override public void ifThenElse(CodeBlock thn, CodeBlock els) { thn.execute(); }
        @Override public Buul and(Supplier<Buul> other) { return other.get(); }
        @Override public Tru or(Supplier<Buul> other) { return this; }
        @Override public Fls not() { return new Fls(); }
      }
    
      static class Fls implements Buul {
        @Override public <T, U extends T, V extends T> T ifThenElse(Supplier<U> thn, Supplier<V> els) { return els.get(); }
        @Override public void ifThenElse(CodeBlock thn, CodeBlock els) { els.execute(); }
        @Override public Fls and(Supplier<Buul> other) { return this; }
        @Override public Buul or(Supplier<Buul> other) { return other.get(); }
        @Override public Tru not() { return new Tru(); }
      }
    }
    
    @FunctionalInterface interface CodeBlock { void execute(); }
    

    这如何适用于Option

    好的,太好了。我写了一篇关于如何使用消息调度实现布尔值的教程。这和Option 有什么关系?嗯,首先,我想在更笼统的层面回答你的问题:

    使用选项类型是否需要 if 语句?

    我想表明您可以始终通过引入适当的类型来消除对if 的需要!这适用于所有类型的条件(if、三元条件运算符?:switch,甚至for 循环、while 循环和foreach 样式循环) 在所有面向对象的语言中。不仅适用于Option,也不仅仅适用于 Java 和 Scala。

    我想首先向您展示更熟悉的布尔示例的另一个原因是,Option 的实现实际上看起来非常相似!

    您有一个名为Option 的抽象基类/interface,并且您有两个具体的实现子类SomeNone。此接口具有允许您转换或操作值的方法,并且这些方法的实现使得None 子类中的实现基本上是 NO-OP。这样,您可以简单地调用这些方法,它们会做某事或什么都不做,但它们不会失败或抛出NullReferenceError 或类似的东西。让我们再次尝试一个简化的示例:

    sealed abstract trait Opt[+T] {
      def map[U](fn: T => U): Opt[U]     // transform value
      def apply(block: T => Unit): Unit  // perform side-effect
      def getOrElse[U >: T](other: U): U // if you want to get the value out, you need to provide a fallback
    }
    
    case class Yep[+T](value: T) extends Opt[T] {
      override def map[U](fn: T => U) = Yep(fn(value))
      override def apply(block: T => Unit) = block(value)
      override def getOrElse[U >: T](other: U) = value
    }
    
    case object Nope extends Opt[Nothing] {
      override def map[U](fn: Nothing => U) = this
      override def apply(block: Nothing => Unit) = ()
      override def getOrElse[U](other: U) = other
    }
    
    val presentValue = Yep(2)
    presentValue(println)
    // 2
    presentValue.getOrElse(42) //=> 2
    
    val missingValue = Nope
    missingValue(println) // nothing happens
    missingValue.getOrElse(42) //=> 42
    

    再一次在 Java 中:

    import java.util.function.Function;
    import java.util.function.Consumer;
    
    import static java.lang.System.out;
    
    public class Test {
      public static void main(String... args) {
        Opt<Integer> presentValue = new Opt.Yep<>(2);
        presentValue.perform(out::println);
        // 2
        out.println(presentValue.getOrElse(42)); // 2
    
        Opt<Integer> missingValue = new Opt.Nope<>();
        missingValue.perform(out::println); // nothing happens
        out.println(missingValue.getOrElse(42)); // 42
      }
    }
    
    interface Opt<T> {
      <U> Opt<U> map(Function<T, U> fn);
      void perform(Consumer<T> block);
      T getOrElse(T other);
    
      static class Yep<T> implements Opt<T> {
        private final T value;
    
        Yep(T value) { this.value = value; }
    
        @Override public <U> Yep<U> map(Function<T, U> fn) { return new Yep<>(fn.apply(value)); }
        @Override public void perform(Consumer<T> block) { block.accept(value); }
        @Override public T getOrElse(T other) { return value; }
      }
    
      static class Nope<T> implements Opt<T> {
        @Override public <U> Nope<U> map(Function<T, U> fn) { return new Nope<>(); }
        @Override public void perform(Consumer<T> block) { return; }
        @Override public T getOrElse(T other) { return other; }
      }
    }
    

    另一种看待它的方式:Option as Collection

    一种完全不同(但在某种意义上非常相似)的看待它的方式是将Option解释为Collection,它可以只有没有元素或只有一个元素。如果你想一想,那就是它,对吧?它是一个空的或具有单个元素的集合。那么,如果我们让Option 实现标准的收集操作/API 会发生什么?

    好吧,例如,我们可以对其进行迭代:当你迭代一个列表时,你并不关心列表中有多少元素。你没有if 声明“如果有十个元素,运行循环体十次,如果有九个元素,运行循环体九次,......”。您只需拥有循环体。如果有十个元素,则执行十次,如果有两个元素,则执行两次,如果有一个元素,则执行一次,如果没有元素,则根本不执行。换句话说:如果您只是在Option 上使用foreach,它将完全按照您问题中假设的if 语句的作用...... 除了没有if 语句!

    像这样:

    for (int i: maybeInt) out.println(i)  // "iterate"
    

    事实上,您可能已经注意到,我上面的perform 方法(Scala 示例中的apply)实际上foreach 的实现!

    当然,我已经暗示了这种解释,通过将转换方法命名为上面的map

    通过使Option 同构为单元素集合,您可以打开集合库处理潜在缺失值的全部功能:您可以“迭代”Option(这将执行一个侧面-效果与否),您可以“转换”它(这将返回新值的Some,或者只是保持None,但不会像null那样抛出异常),您可以“展平”它(这将返回一个单级Some(例如Some(Some(Some(23)))Some(23)),如果你有一个Somes的嵌套塔,或者None,如果有一个None塔(例如Some(Some(Some(None)))None)),你可以flatMap它(它允许你安全地“链接”转换),等等。

    但是等等……还有更多!

    最后但同样重要的是:Option 作为 monad

    Option 不仅是一个集合,它还是一个 monad!在这个已经太长的答案中,我不会介绍 monad 是什么。

    Option 作为 monad 给我们带来的好处之一是能够“链接”可选值的计算。事实上,上面提到的mapflatMap 是正确实施后使Option 成为monad 的操作。几种语言具有用于计算和链式计算单值的内置语法糖:Haskell 有 do-notation,Scala 有 for-comprehensions,C♯ 有 LINQ 查询表达式。

    例如,在 Scala 中,您可以这样说:

    for (i ← maybeInt) println(i)  // "iterate"
    for (i ← maybeInt) yield i * i // "transform"
    

    在 C♯ 中:

    from i in maybeInt select Console.WriteLine(i) // "iterate"
    from i in maybeInt select i * i                // "transform"
    

    Daniel Westheide 的这篇文章也很好地介绍了 Option 作为集合和单子的解释:

    【讨论】:

    • 不错的一个! Buul 实际上也是布尔值Church encoding 的变体。
    • 好帖子!两个挑剔: 1. “实际上,上面提到的 map 和 flatMap 是正确实现的操作,使 Option 成为 monad。”,不应该是pure(应用)和@ 987654417@? 2. LINQ 查询的 C# 示例无法编译。在 C# 中,select 用于当想要投影类型 A =&gt; B,但 C# 的 void(这是 Console.WriteLine 返回的内容)不是有效的返回类型,这与 Scala 的 Unit 类型不同。跨度>
    【解决方案3】:

    不使用null 的最大优势在于,无论何时使用Option,类型检查器都知道你不需要需要编写一张支票。

    由于经常需要这种检查,因此很容易出错(无论是做得太多还是做得不够)。但是当您使用Option 时,您正在征用类型检查器来告诉您是否需要进行检查。

    Dima's answer 也很重要:Option 类型提供了很多有用的工具。类型检查器可帮助您确保正确使用它们!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-05-12
      • 1970-01-01
      • 2018-04-19
      • 1970-01-01
      • 2016-08-05
      • 1970-01-01
      • 1970-01-01
      • 2016-12-05
      相关资源
      最近更新 更多