Free your mind
If you think of Option
as Nullable
by a different name then you are absolutely correct - Option
is simply Nullable
for reference types.
The Option
pattern makes more sense if you view it as a monad or as a specialized collection that contain either one or zero values.
Option
as a collection
Consider a simple foreach
loop with a list that cannot be null
:
public void DoWork<T>(List<T> someList) {
foreach (var el in someList) {
Console.WriteLine(el);
}
}
If you pass an empty list to DoWork
, nothing happens:
DoWork(new List<int>());
If you pass a list with one or more elements in it, work happens:
DoWork(new List<int>(1));
// 1
Let's alias the empty list to None
and the list with one entry in it to Some
:
var None = new List<int>();
var Some = new List(1);
We can pass these variables to DoWork
and we get the same behavior as before:
DoWork(None);
DoWork(Some);
// 1
Of course, we can also use LINQ extension methods:
Some.Where(x => x > 0).Select(x => x * 2);
// List(2)
// Some -> Transform Function(s) -> another Some
None.Where(x => x > 0).Select(x => x * 2);
// List()
// None -> None
Some.Where(x => x > 100).Select(x => x * 2);
// List() aka None
// Some -> A Transform that eliminates the element -> None
Interesting side note: LINQ is monadic.
Wait, what just happened?
By wrapping the value that we want inside a list we were suddenly able to only apply an operation to the value if we actually had a value in the first place!
Extending Optional
With that consideration in mind, let's add a few methods to Optional
to let us work with it as if it were a collection (alternately, we could make it a specialized version of IEnumerable
that only allows one entry):
// map makes it easy to work with pure functions
public Optional<TOut> Map<TIn, TOut>(Func<TIn, TOut> f) where TIn : T {
return IsPresent ? Optional.For(f(value)) : Empty();
}
// foreach is for side-effects
public Optional<T> Foreach(Action<T> f) {
if (IsPresent) f(value);
return this;
}
// getOrElse for defaults
public T GetOrElse(Func<T> f) {
return IsPresent ? value : f();
}
public T GetOrElse(T defaultValue) { return IsPresent ? value: defaultValue; }
// orElse for taking actions when dealing with `None`
public void OrElse(Action<T> f) { if (!IsPresent) f(); }
Then your code becomes:
Optional<ICustomer> optionalCustomer = repository.FindCustomerByName("Matt");
optionalCustomer
.Foreach(customer =>
Console.WriteLine("Customer found: " + customer.ToString()))
.OrElse(() => Console.WriteLine("Customer not found"));
Not much savings there, right? And two more anonymous functions - so why would we do this? Because, just like LINQ, it enables us to set up a chain of behavior that only executes as long as we have the input that we need. For example:
optionalCustomer
.Map(predictCustomerBehavior)
.Map(chooseIncentiveBasedOnPredictedBehavior)
.Foreach(scheduleIncentiveMessage);
Each of these actions (predictCustomerBehavior
, chooseIncentiveBasedOnPredictedBehavior
, scheduleIncentiveMessage
) is expensive - but they will only happen if we have a customer to begin with!
It gets better though - after some study we realize that we cannot always predict customer behavior. So we change the signature of predictCustomerBehavior
to return an Optional<CustomerBehaviorPrediction>
and change our second Map
call in the chain to FlatMap
:
optionalCustomer
.FlatMap(predictCustomerBehavior)
.Map(chooseIncentiveBasedOnPredictedBehavior)
.Foreach(scheduleIncentiveMessage);
which is defined as:
public Optional<TOut> FlatMap<TIn, TOut>(Func<TIn, Optional<TOut>> f) where TIn : T {
var Optional<Optional<TOut>> result = Map(f)
return result.IsPresent ? result.value : Empty();
}
This starts to look a lot like LINQ (FlatMap
-> Flatten
, for example).
Further possible refinements
In order to get more utility out of Optional
we should really make it implement IEnumerable
. Additionally, we can take advantage of polymorphism and create two sub-types of Optional
, Some
and None
to represent the full list and the empty list case. Then our methods can drop the IsPresent
checks, making them easier to read.
TL;DR
The advantages of LINQ for expensive operations are obvious:
someList
.Where(cheapOp1)
.SkipWhile(cheapOp2)
.GroupBy(expensiveOp)
.Select(expensiveProjection);
Optional
, when viewed as a collection of one or zero values provides a similar benefit (and there's no reason it couldn't implement IEnumerable
so that LINQ methods would work on it as well):
someOptional
.FlatMap(expensiveOp1)
.Filter(expensiveOp2)
.GetOrElse(generateDefaultValue);
Further suggested reading