Tony Hoare calls null references his billion dollar mistake. Using null
values (NULL
, Null
, nil
, etc) makes code harder to maintain and to understand.
But what can we do about it? To start let's review the meaning of null
values ...
Modeling optional results
Finding Customers
Finding operations are very common. Given a Customer
class, a Find
method could look like this:
1 | public Customer Find(Query query) ; |
Now what happens when the customer can not be found? Possible options are:
-
Throw an exception: Why though? Where is the exceptional case? Trying to find a
Customer
has the possible scenario of not beign found. -
Return
null
to mean nothing was found. But are we positive we are getting anull
for that reason and not other? And what can we do with thenull
value after? -
Return a
NullObject
that representsCustomer
null value. That could work to show some of the customer's data, if we expect strings or something similar. But for most cases this won't be enough.
Parsing Integers
In C# you can use Int32.parse
or Int32.tryParse
to do the job. The former throws an Exception
when the string can not be parsed into an int and the later returns a Bool
indicating if the operation succeeded using an out
parameter for the value.
The first approach is not that intuitive. I want to get a result, not to catch an exception.
The second one with the boolean result seems to go in the right direction but having an out
parameter complicates things, and makes it hard to understand and hard to pass to another function, etc.
Optional values
F# has a very simple way to deal with this by creating a Discriminated Union with only two values:
1 | type Option<'T> = |
Now finding customers has a very clear interface:
1 | let tryFindCustomer (q: query) : Option<Customer> |
Parse clearly can succeed giving the parsed result or fail, therefore the result is Optional
.
1 | let tryParseInt (s:string) = |
NOTE: out parameters are converted tuples in F#.
As a convention is common to call functions trySomeAction
when the action can fail and return an optional value.
Working with optional values
Modeling optional values is a great start. Using an optional value makes very clear the fact that the caller has the responsibility to handle the possiblity of having None
as a result.
Having clear meaning improves clarity, intent, error handling, and there is no null
that can cause problems.
However, in terms of handling the result, are we that much better than before?
Of course we could check always for Some
or None
and handle the result, but where is the fun
in that?
The key to create a great abstraction is usage. After seeing the same bit of code used again and again we could be confident that abstracting the behaviour is going to be really useful.
To avoid doing a match
on Option
types luckily we have a series of helper functions that address most common scenarios.
Many of these functions live in the FSharpx library.
Using defaults
To obtain the value from an Option
we can use Option.get
:
1 | let get = |
(what's that function
? Here is an explanation about pattern maching function)
Notice that get
throws an exception when there is nothing to get.
Throwing exception (and catching it) is ok, but makes hard to compose the result or transform it.
Instead why not use a default value when there is None
? (Taken from FSharpx):
1 | let getOrElse v = |
Knowing that it can fail, and having a default value help us write code that can use a default value and keep going:
1 | let doWebRequest countStr = |
Applying functions
Another common scenario is to apply a function when we get a result or just do nothing otherwise:
For example, the following code will only print the message when tryParse
returns Some
:
1 | let doWebRequest param = |
And the implementation:
1 | let iter f = |
Shortcircuit None
iter
is useful to execute a function when there is Some
value returned, but is common to use the value somehow and transform it into something else.
For that we can use the map
function:
1 | let map f = |
This is a bit different. Not only the function passed as parameter is applied after unboxing the value, but the result is boxed back into an Option
. Once an Option
always an Option
.
Imagine a railway and a train that once hits the value None
switches to a None
railway and bypasses any operation that comes after. Thus the shortcircuit.
1 | let queryCustomers quantity = [] // silly implementation |
Mapping to Option
Another common case, very similar to map
, is to use a function that transforms the boxed value and returns an Option
.
1 | let bind f = |
For example the parameter that represents the customer id is a string, and we need to parse it into an int. Getting the parameter can return a None
and the parse function could return a None
as well.
1 | let doWebRequest req = |
Multiple optional values
So far so good with one parameter. But what happens with more than one parameter? The goal is to shortcircuit and if one of the parameters is None
then abort and just return None
.
One option is to use FSharpx and the MaybeBuilder
. I'm not going to discuss the details of how builders work but I will show you the practical usage to illustrate the point.
1 | type Result<'TData> = |
In this scenario we have a happy path:
- All parameters are present, then the result is
Success
with the output offindCustomers
.
And four unhappy paths:
count
is not present, then themaybe
builder does shortcircuit toNone
andgetOrElse
returns anError
.city
is not present, then themaybe
builder does shortcircuit toNone
andgetOrElse
returns anError
.country
is not present, then themaybe
builder does shortcircuit toNone
andgetOrElse
returns anError
.count
is present, but can not be parsed then ... shortcircuit ... andError
.
The let!
is doing the unboxing from Option
to the actual type, and when any of the expressions has the value None
then the builder does the shortcircuit and returns None
as result.
Applicative Style
Another way to write the same concept (sometimes a bit more clear) is to use the Applicative style by using operators that represent the operations that we already are using that also apply shortcircuit when possible.
For the functions we have used in the Option
type the operators are:
1 | <!> // is an alias for map |
(Find all the definitions here).
To use them let's try to read the parameters from the request.
1 | let city = req.tryGetParam "city" |
Good, now count
needs to be parsed as well, so we can use tryParse
that returns an Option
. What can we use when we need to apply a function that returns also an Option
? Bind
of course, or >>=
.
1 | let count = req.tryGetParam "count" >>= Int32.tryParse |
All the parameters are parsed into Option
and findCustomers
can be invoked.
1 | findCustomers <!> count ???? city ??? country |
To apply a function over an Option
we can use the operator <!>
(map
), but what about the other two parameters?
Let me rephrase, what happens when we apply a function that takes three parameters to just one parameter? Exactly! A partial application!
Same happens when applying the operator <!>
to a function that takes three parameters, the difference is that the partially applied function gets boxed in an Option
.
1 | let count = req.tryGetParam "count" >>= Int32.tryParse |
Now we need to apply the boxed function to a boxed value, and for that we can use the operator <*>
that takes a boxed function and a boxed value and returns the boxed version of applying the function to the value.
1 | findCustomers <!> count <*> city ... // partial application of the function to city |
In this case we have two more parameters so the full version would be:
1 | let findCustomers count city country = [] |
Summary
Using an Option
to represent when a value may not be present has many advantages.
Not only is easier to deal with cases that produce no results, but also the code is clear an easy to follow.
Though here the code is in F# you could implement similar features in your favourite language.
(Check out Java 8 Optional class or this C# implementation of Maybe by Adam Krieger).
All the code can be found here. I included a series of tests that show how the builder and applicative style apply a shortcircuit when one of the parameters is missing.
Thanks to my good friend Shane for helping me to test the code in all platforms.