So, it's pretty well known that the infamous NullReferenceException
is the most common exception in software products. I've been reading some articles, and found myself with the Optional approach.
Its aim is to create some kind of encapsulation around a nullable value
public sealed class Optional<T> where T : class {
private T value;
private Optional(T value) {
this.value = value;
}
//Used to create an empty container
public static Optional<T> Empty() {
return new Optional(null);
}
//Used to create a container with a non-null value
public static Optional<T> For(T value) {
return new Optional(value);
}
//Used to check if the container holds a non-null value
public bool IsPresent {
get { return value != null; }
}
//Retrieves the non-null value
public T Value {
get { return value; }
}
}
Afterwards, the now optional value can be returned like this:
public Optional<ICustomer> FindCustomerByName(string name)
{
ICustomer customer = null;
// Code to find the customer in database
if(customer != null) {
return Optional.Of(customer);
} else {
return Optional.Empty();
}
}
And handled like this:
Optional<ICustomer> optionalCustomer = repository.FindCustomerByName("Matt");
if(optionalCustomer.IsPresent) {
ICustomer foundCustomer = optionalCustomer.Value;
Console.WriteLine("Customer found: " + customer.ToString());
} else {
Console.WriteLine("Customer not found");
}
I don't see any improvement, just shifted complexity.
The programmer must remember to check if a value IsPresent
, in the same way he must remember to check if a value != null
.
And if he forgets, he would get a NullReferenceException
on both approaches.
What am I missing? What advantages (if any) does the Optional pattern provide over something like Nullable<T>
and the null coalescing operator?
Patterns don't provide solutions, they inspire solutions. Patterns explicitly capture expert knowledge and design tradeoffs and make this expertise widely available. Ease the transition to object-oriented technology.
Design patterns are programming language independent strategies for solving a common problem. That means a design pattern represents an idea, not a particular implementation. By using design patterns, you can make your code more flexible, reusable, and maintainable.
The Observer pattern provides you with the following advantages: It supports the principle of loose coupling between objects that interact with each other. It allows sending data to other objects effectively without any change in the Subject or Observer classes. Observers can be added/removed at any point in time.
Disadvantages. High degree of flexibility. High complexity of software (especially decorator interface) Expansion of function of classes without inheritance. Not beginner-friendly.
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 collectionConsider 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.
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!
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).
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.
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);
null
is not enough (C#)it would probally make more sense if you used something like this
interface ICustomer {
String name { get; }
}
public class OptionalCustomer : ICustomer {
public OptionalCustomer (ICustomer value) {
this.value = value;
}
public static OptionalCustomer Empty() {
return new OptionalCustomer(null);
}
ICustomer value;
public String name { get {
if (value == null ) {
return "No customer found";
}
return value.Name;
}
}
}
now if your pass an "empty" optional customer object you can still call the .Name property (without getting nullpointers)
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