Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does using option type remove need for if statements?

I'm reading Functional Programming in Scala and here are two of the advantages it provides for using the Option type instead of checking get for null :

  1. It allows errors to silently propagate—the caller can forget to check this condition and won’t be alerted by the compiler, which might result in subsequent code not working properly. Often the error won’t be detected until much later in the code.

  2. Besides being error-prone, it results in a fair amount of boilerplate code at call sites, with explicit if statements to check whether the caller has received a “real” result. This boilerplate is magnified if you happen to be calling several functions, each of which uses error codes that must be checked and aggregated in some way.

For point 2. although do not have to check for null still have to check if the Option type contains a Some or None. The if check is not removed , it's just made more explicit in that instead of a type being null it may not contain a value. Is this a correct interpretation?

Although I cite a Scala book the Option type is also available in Java 8 and so I think is valid in Java also section also.

like image 350
blue-sky Avatar asked Dec 24 '22 07:12

blue-sky


2 Answers

The idea is that rather than bothering to check whether your Option is Some or None, you simply transform it into another Option, using a series of monadic operations or simply provide a default value in the end. Imagine something like this in 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;
 }

There is a whole bunch of ways to write it in scala, each of which is way prettier than this. For example (provided, that all "nullable" objects are represented by an Option):

 def getUserApartmentNumber(user: Option[User]) = user
   .flatMap(_.address)
   .flatMap(_.building)
   .flatMap(_.apartment)
   .map(_.getNumber)
   .getOrElse(-1)
like image 171
Dima Avatar answered Jan 13 '23 12:01

Dima


Excursion: Truly Object-Oriented Booleans

Both Scala and Java are object-oriented languages. In an object-oriented language, you never need ifs for anything! You can always replace conditionals with polymorphic message dispatch. (Mentally replace "message" with "virtual method", if you are not familiar with Smalltalk terminology.)

What does message dispatch do? Well, basically, it says "if the object has this class, then execute this method, if it has that class, then execute that other method", and so on. Do you see? Message dispatch is already a conditional! The Smalltalk language doesn't even have conditionals (it also doesn't have loops), instead it uses polymorphic message dispatch.

The basic idea is this: you have a protocol / interface / abstract class for booleans which has a method named, say, ifThenElse taking two code blocks (lambdas, functions) as arguments. And you have two concrete implementation subclasses, one for "true" values which implements ifThenElse such that it executes its first argument and simply ignores the second, and another concrete implementation subclass for "false" values, where ifThenElse executes the second argument and ignores the first.

If you think about it, this is really just the Replace Conditional With Polymorphism Refactoring taken to the logical extreme and applied to if/then/else itself.

It looks kind of like this (contrived) Scala example:

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

Which in Java would look something like this:

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();
}

and and or can be implemented in a similar way. Let's ignore short-circuiting for the moment. What is true && something? Well, it only depends on something, doesn't it? If something is false, the result is false, if something is true, the result is true, or in other words: the result is always just something. So, we can implement and in our "true" subclass simply as Boolean and(Boolean other) { return other; }.

The same applies to false || something.

Likewise, for false && something and true || something, the result is always the first operand, or in other words: return this;.

Now, in order to get short-circuiting, all we have to do is to wrap the argument in a function, and instead of returning the argument, call the function and return its result.

Here's the full examples in Scala and 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(); }

How does this apply to Option?

Okay, great. I have written a tutorial on how to implement booleans using message dispatch. What does this have to do with Option? Well, firstly, I wanted to answer your question on a more general level:

Does using option type remove need for if statements?

I wanted to show that you can always remove the need for if by introducing the appropriate type! This is true for all sorts of conditionals (if, the ternary conditional operator ?:, switch, even for loops, while loops, and foreach-style loops) in all object-oriented languages. Not just for Option and not just for Java and Scala.

The other reason why I wanted to show you the more familiar example of booleans first, is that the implementation of Option actually looks very similar!

You have an abstract base class / interface called Option, and you have two concrete implementation subclasses called Some and None. This interface has methods that allow you to transform or manipulate the value, and the methods are implemented such that the implementations in the None subclass are basically NO-OPs. That way, you can simply call those methods, and they will either do something or do nothing, but they will not fail or throw a NullReferenceError or something like that. Let's again try a simplified example:

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

And again in 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; }
  }
}

Another way to look at it: Option as Collection

A completely different (and yet, in some sense, very similar) way to look at it, is to interpret Option as a Collection that can only ever have either no elements or exactly one element. If you think about it, that's what it is, right? It's a collection that is either empty or has a single element. So, what will happen, if we have Option implement the standard collection operations / APIs?

Well, for example, we can iterate over it: when you iterate over, say, a list, you don't care how many elements there are in the list. You don't have an if statement for "if there are ten elements, run the loop body ten times, if there are nine elements, run the loop body nine times, …". You simply have the loop body. If there are ten elements, it gets executed ten times, if there are two elements, it gets executed twice, if there is one element, it gets executed once, and if there is no element, it doesn't get executed at all. In other words: if you just foreach over an Option, it will do exactly what the hypothetical if statement from your question does … except without the if statement!

Like this:

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

In fact, you may have noticed, that my perform method from above (apply in the Scala example), actually is the implementation for foreach!

And of course, I have already hinted at this interpretation, by naming the transformation method map above.

By making Option isomorphic to a single-element collection, you open up the whole power of the collection library to deal with potentially missing values: you can "iterate" over an Option (which will either perform a side-effect or not), you can "transform" it (which will either return a Some of the new value, or simply stay a None, but without throwing an exception like a null would), you can "flatten" it (which will return a single-level Some (e.g. Some(Some(Some(23)))Some(23)), if you have a nested tower of Somes, or None, if there is a None in the tower (e.g. Some(Some(Some(None)))None)), you can flatMap it (which allows you to "chain" transformations safely), and so on.

But wait … there's more!

Last but not least: Option as a monad

Not only is Option a collection, it is also a monad! I won't cover what a monad is here in this already much too long answer.

One of the things Option being a monad buys us, is the ability to "chain" computations on optional values. In fact, map and flatMap mentioned above, are the operations that, correctly implemented, make Option a monad. Several languages have built-in syntactic sugar for computations and chaining computations on monadic values: Haskell has do-notation, Scala has for-comprehensions, C♯ has LINQ Query Expressions.

For example, in Scala, you could say something like this:

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

And in C♯:

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

The interpretation of Option as a collection and as a monad is also covered very well in this article by Daniel Westheide:

  • The Neophyte's Guide to Scala Part 5: The Option Type
like image 30
Jörg W Mittag Avatar answered Jan 13 '23 13:01

Jörg W Mittag