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
:
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.
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.
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)
Both Scala and Java are object-oriented languages. In an object-oriented language, you never need if
s 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(); }
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; }
}
}
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 Some
s, 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!
Option
as a monadNot 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:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With