There are a couple of fundamental ideas behind this.
The first fundamental idea is that in Object-Oriented Programming, we program by composing systems of autonomous objects that collaborate by sending messages to each other.
if statement is not a message send, therefore, it is not Object-Oriented.
Dynamic runtime-polymorphic message dispatch (virtual method dispatch in C# jargon) is more general, more powerful, and more expressive than conditionals, so it should be used whenever possible.
The Replace Conditionals with Polymorphism Refactoring shows us how to get rid of conditionals in some special cases, and languages like Smalltalk which don't even have conditionals and loops serve as existence proof that it is always possible to get rid of conditionals and loops and replace them with polymorphic message dispatch.
If you are interested in this train of thought, you can find some resources at the Anti-
The Introduce Special Case Refactoring (also known as Introduce Null Object) can help getting rid of conditionals handling special cases, and specifically
null is Evil
While the first idea was deeply rooted in OO, this idea is deeply rooted in statically-typed functional programming, logic, and maths.
There is a deep connection between logic and programming which is exemplified by the Curry-Howard Isomorphism, Girard-Reynolds Isomorphism, Wadler-Blott, and many others. Basically, every type system is equivalent to a system of logic, and every logic is equivalent to a type system. In this isomorphism, [types correspond to theorems and (well-typed) programs correspond to proofs of those theorems].
The problem is that
null is a valid program for every type (or at least every non-primitive reference type). Which means that
null can prove every theorem! This essentially breaks logic and thus breaks the type system.
Consider a function of type
Account -> Money. This can be interpreted as logical implication, i.e. as "from
Account, I can deduce
Money", or a bit more pragmatically "given an
Account, I can produce
Money". (You can imagine that this is basically the
balance function.) Now, I can "prove" this theorem by implementing the
balance function properly, but I can also prove it by simply returning
null. The type checker will allow me to do that.
Even worse, the type checker will also allow me to implement, say, a function
Cow -> Rain that way, which is clearly non-sensical.
That's like saying that burning all the pieces and storming off the playing venue is a legitimate way of winning a LEGO building contest.
The solution is to create a specific type that signifies the (potential) absence of a value. This type is often called
Optional. In languages with Algebraic Datatypes, it is modelled as a Sum Type.
Such a type either contains a value or it doesn't. If you look at it in a certain way, it almost looks like a collection that is either empty or contains a single element.
And this is the great power that such a type gives you: IFF you implement it like a collection, then you get all the power of collections for free! (It is really sad that the authors of Java's
Optional type did not understand this.)
How do you produce a new value from a value that may be absent? Well, what happens when you
map an empty Collection? Nothing! What happens when you
map a collection with a single element? You get a new collection with a single transformed element.
maybeAbsent != null ? someFunction(maybeAbsent) : null
How do you perform a side-effect with a value that may be absent? Well, what happens when you iterate over an empty collection? Nothing! What happens when you iterate over a collection with a single element? The side-effect gets executed once with the element.
if (maybeAbsent != null) Console.WriteLn(maybeAbsent);
foreach (var option in maybeAbsent) Console.WriteLn(option);
This starts to really shine when you have complex chains of computations that may or may not produce a value. Then you have operations such as
SelectMany in .NET) which allow you to "thread" an optional value through a long chain of computations, or
flatten which allows you to remove nested levels of "optionality".
It turns out that such an optional type is actually much more general than a collection: it is a Monad and in fact even a Functor. Which gives you additional powers especially in languages that have special notations for Monads like C# (LINQ Query Expressions), Scala (
for comprehensions), and Haskell (
Combine the powers!
It turns out that Algebraic Datatypes can be nicely mapped to Inheritance. (Scala has some features such as
sealed classes and
objects that make it even nicer, but those are not strictly necessary.) This gives you the combined power of getting rid of
nulls by modeling them as optional types, and using polymorphism by implementing the operations on the two subtypes (for example
SomeValue<T>) accordingly. E.g.
SomeValue<T>.Select(Func<T> f) => new SomeValue(f(this.Value)) and
NoValue.Select<T>(Func<T> _) => this.
You might (rightfully) ask yourself: so, how do I get the value out of the
Option at the end? One nice property is that sometimes, you actually don't even need to do that! If all you want is to perform a side-effect, for example, then you never need to get the value out, you can just use
In functional languages, you would typically use case discrimination via pattern matching. You can do the same in OO languages, e.g. via a
if (or even pattern matching in C#), but can we get rid of these conditionals? It turns out we can! We just need polymorphism again, and we add a method to our type which gets the value out but takes an alternative default value as its argument, and we implement the two versions like this:
SomeValue<T>.GetOrElse(T _) => this.Value and
NoValue.GetOrElse<T>(T defaultValue) => defaultValue.
Both of these have another powerful advantage over dealing with
null: you can't forget handling it! Type systems of functional languages typically support exhaustiveness checks for pattern matching that make it a compile error if you don't handle all cases. And in our OO example, we have made sure that the only way to get the value back out is to call the
GetOrElse method to which we must pass an argument.
Conversely, it is impossible to accidentally pass a potentially missing value to a function that doesn't expect it because
Option<T> is simply a different (and incompatible) type than
This gives us the four main advantages of using
Option types instead of
null references to model the potential absence of a value:
- No conditionals, no explicit checks
- Easy chaining
- Exhaustiveness checks
- Potentially missing values are clearly separated and explicitly marked as distinct types
There are some alternatives to the approach described above.
One would be to explicitly track
nulls as was done in Spec# or separate the type system and all references into two distinct spaces (nullable and non-nullable). This does not solve the problem, though: if you have a nullable reference, you still need to check it. Also, it introduces complexity: assuming a starting point of a typical OO language, circa C# 2.0 / Java 5 -ish, whereas the approach described above only needs language features that exist anyway (inheritance and generics) and would even allow you to get rid of a language feature (namely
null), this requires adding language features (nullable and non-nullable references).
Another would be adding so-called "
null-safe" and "
null-coalescing" operators to the language (as was done in recent versions of C#). While these make dealing with
null less annoying, they don't solve the fundamental problem. In fact, I think that by making it easier to work with
null, they reduce the pain, and thus the pressure and the incentive to fundamentally solve the problem, and thus in some sense actually make the problem worse.
Specifically in .NET, there is also the
bool TrySomething(SomeType arg, out SomeOtherType result) idiom, which can be trivially replaced with
Option<SomeOtherType> Something(SomeType arg).