Optionals
In this post I want to talk about Optionals more deeply. I already wrote
about null, but I noticed that it is still
not immediately clear on why Optionals are better. Instead of focusing why null
is bad, this time I want to focus why Optionals are good. For this purpose I also wrote
a small application that I will cover. But first, let’s go over Optionals and see
which benefits they have.
null
is not the problem
At first, I will state that null
itself is not even the problem.
Let’s assume we write a application and we need to read some Product entries from a
database. We just have a simple id as an int
to identify a product.
Now let’s assume we get some user input that tells us to show a Product with the id 12345
.
Sure, as long we only get request to products that exists, we don’t run into any kind of problems.
But let’s imagine we write a function that returns a Product, and the requested Product doesn’t
exists. What do we do in such a case?
The usual approach, at least in OO programming, is to either return null
, or throwing an
Exception. But i don’t think that Exceptions are better compared to null.
So, why is returning null
bad? Because a client calling our function is not forced to check
for null
. And if he works with null
as if he had a Product, the program will crash. What
happens if we throw an Exception? Well, our program still crashes!
The thing is. No matter if we return null
or we throw exceptions. We must handle the case when
we don’t get a Product. But null
or exceptions don’t force you to handle both cases.
Neither null
or exceptions forces you to add checks. Both concepts just crashes your program
at runtime. The only hope you have is that you hopefully have a test-suite that hopefully
handles those cases. For me, that is too much of hope.
Yes, throwing an Exception is a little bit better. But think on why it is a little bit better.
We see it as an advantage as we hopefully (gosh!) already see any kind of error during development,
so we can add the needed try/catch
statements.
But wouldn’t it be better if we are forced to add the checks because the language forces us to
do it? Because we otherwise get a compile-time error? If we somehow could get this kind of behaviour
it would mean we never can forgot to add a null
check or a try/catch
statements. It will become
impossible to write programs that unexpectedly crashes at runtime.
Our program either compiles fine, and we handled all places where a no value could be returned, or if we forgot to handle such a place we just get a compile-time error that gives us the exact place and line where we forgot to handle such a case.
It seems like a dream. But this is exactly what Optionals gives us!
Optionals
Optionals fixes that problem because it makes the idea of No value it’s own type. The option
type in F# is defined as followed:
|
|
What does that mean? You can compare it to a bool
or an enum type. We have two cases. Like
a bool
that either can be true
or false
. Here we have an option
that either can be Some
or None
. The difference is that the Some
case can carry an additional value. Something
that an enum or a bool
cannot do.
The Some
or None
are basically constructors. In the same sense that true
or false
are
constructors that creates a bool
. As None
don’t carry any value, you just can write None
,
the same you just can write null
. But, if a function has a path that returns None
, you
must ensure that all code paths return an option
type. As we cannot create functions with
mixed return types. So you cannot write something like this:
|
|
This would either return a string
or an option
. So what you must do is wrap your value in a Some
.
|
|
This has some implications:
- Now you have a function that returns an
option
containing astring
. - A user can see that
containsE
not always returns a value. - A user that wants to work with the result of
containsE
first must check which case he got.
The last implication means you must check if you either got Some
or None
|
|
But we also have a lot of helper functions in the Option
module like Option.map
,
Option.iter
, Option.bind
and so on that helps us working with option
types.
For example it is quite common that we want to check if we have Some value
and call a
function with value
. But what do we do if we have None
? Then we can’t call
our function. This kind of stuff is what a Option.map
does. So instead of
|
|
we also can just write
|
|
Now, we are forced to handle option
values. But option
itself is it’s own type and we have
a lot of functions that helps us working with option
values.
The Application
The best way to show the benefits and the difference is to go through a small example. For that purpose I created a small in-memory database and a CLI program with basic CRUD operations. You can see the full source code at the end. But i don’t want to cover every detail. I want to focus on the Optional part. First, let’s see how we can use the program
Command: show
Id | Name | Price
1 | TV | 499.99
2 | A Book | 29.99
3 | Game Console | 349.99
Command: asdasdfkhjb
Error: invalid command -- [asdasdfkhjb]
Command: delete 2
Command: show 2
Product with id 2 doesn't exists.
Command: insert "Zelda: Skyward Swords" 49,99
Command: show
Id | Name | Price
1 | TV | 499.99
3 | Game Console | 349.99
4 | Zelda: Skyward Swords | 49.99
Command: name 1 "Television"
Command: price 3 299,99
Command: show
Id | Name | Price
1 | Television | 499.99
3 | Game Console | 299.99
4 | Zelda: Skyward Swords | 49.99
Command: exit
The program just contains the commands show
, insert
, name
, price
, delete
and exit
as
valid commands. As you can imagine we need to handle a lot of failures.
As usual, all kind of user input can be invalid. A command that doesn’t exists. Parsing of a number failed, in general a user just enters garbage. Or a user tries to change or show a specific entry that doesn’t exists.
As i don’t want to cover the whole program, just let’s look first at the modules and functions and their signatures to get a overview of the code.
So let’s just go through some of the interesting parts. At first, i added a Option.IfNone
function. It either returns a value if present or the provided value. It is just a call
to defaultArg
. I created ifNone
because the default argument order of defaultArg
doesn’t work nicely with piping.
|
|
Here is a brief overview of the modules and functions i created.
|
|
For the in-memory database i just used a Map
without creating a new type,
that’s why you see a lot of Map
types. But overall you still see that we don’t have
so few functions returning option
.
At first we have the Product
module. It just provides some helper functions to create and
modify the following Product
Record type.
|
|
The Product
type should contain all our Business logic, validation and so on. It should
only contain pure functions, usually this modules should contain the most code, but
in this example Product
doesn’t really do much, so it is the smallest module.
DB
is basically an Application layer or Service layer. It just provides the code
to save things to a database or read things from a database. It is just a simple
Key/Value interface. As you can see from the types. It is fully generic and could
work with any type.
From the types, only getById
returns an option
. But that doesn’t mean it is the only
function that has some option
handling in it.
Finally, on top we have the CLI
module. The purpose of it is to provide the logic
for the CLI
. We parse our input string, interpret the commands and update the database.
DB
Map
itself is an immutable type. So the general idea is that operations like
insert
, delete
, update
return the new state of Map
.
|
|
Our getbyid
function is fairly simple, as it is just a call to tryFind
. But it makes
sense to talk about tryFind
and compare it with a Dictionary
. If you use a Dictionary
in C# and you try to fetch an entry you usually have two different behaviours.
Either you choose that retrieving an entry could throw an exception, or you use the
TryGetValue
method. It doesn’t throw an exception, but instead it returns a bool
that
you must check, to get the value you have to pass an out
parameter to TryGetValue
.
I don’t really like both ways. We don’t want exceptions and wrap the code in try/catch
.
But also TryGetValue
is annoying as we first create a (mutable) value and pass
it to TryGetValue
.
In F# on the other hand a function like tryFind
just returns an option
, we
either have Some value
or None
. Here we just return the option
as-is. That is
also the reason why getById
returns an option
. We don’t must immediately check the
option
. We also can just pass it as a value around. We only must check it if we need
the value inside Some
.
|
|
Inside DB
i added a updateId
function. Usually i wouldn’t add such a function, but
on the other hand, it is useful and at the same time interesting. The purpose of this
function is that we can fetch and update an entry at the same time. The interesting is:
updateId
don’t return an option
.
At first we fetch the specified entry with getById
that returns an option
.
When we got a result we want to transform the value
. That’s why we have (f value)
in-there. The result of this is passed to update, that then returns a new updated
Map
. But what happens if getById
returns a None
instead of a value? The Option.map
part will also directly return None
instead of executing the update
call.
But we always want to ensure to return a Map
, so we either return a new updated Map
or in the case we had None
. Option.ifNone
returns the Map
we started with, without
any change applied.
Assume you work in a language without option
and you get a null
. You theoretically could
write.
|
|
But you will only notice that this is error-prone. As this code can just throw a
NullReferenceException
if you try to update a non-existent value. Sure you then could add
the typical null
checks to get it safe.
|
|
The difference is, with an option
you are forced to handle that case. You cannot
write error-prone code in the first-place! But in general it also shows that updateId
even the fact that an id
could not be presented, still don’t return an option
. Actually
we have an operation that cannot fail here. Either it updates an entry, or it does
nothing. And you get that behaviour ensured by the type-system at compilation-time!
CLI
The CLI handling is in general split into two parts. First I parse the input by the user
and I use a Discriminated Union to save the different input commands. The type is just
named Command
. parseCommand
do the transformation of converting the input string
to such a Command
. The interesting thing is. Parsing could fail, but you don’t see
a Command option
for this function.
This is a general idea. Sure option
is nice, but if you anyway build a custom type,
you can make the “None”, “NotExistence” or “Invalid” a part of your type. Here I just
have a Command
that contains an Invalid
case.
|
|
In general you can see that the Discriminated Union just contains a case for every CLI command,
it also contains the parsed input as int
, string
or decimal
. In my code
I just use Pattern Matching, but with Active Patterns I can specify transformation
functions on top of it.
|
|
LC
in that example is a Complete Active Pattern. As it will always succeed. LC
just
takes a string
and turns it into a lower-case
string. I use it like that
| [| LC "show" |] -> Show
This not only a Pattern Match that checks if we have an array with “show” as the only entry. It first transform the entry to a lower-case string and then compares it with “show”.
Transforming a string to a lower-case string always succeed, but we also can use
Partial Active Patterns for operation that could fail. That is what (|Int|_|)
and
(|Decimal|_|)
stands for. Both operation try to parse a string as either Int
or
Decimal
. In a success case we return Some
, otherwise None
. But the handling of those
option
is done for us. You also see something like that in other functions.
For example a List.choose
is basically a map
and then a filter
operation in one
step. You not only can transform an entry to a new value. By returning Some
or None
you also can filter. The choose
operation only take the Some
elements. Here we have
the same. Using Options for a success/failure or filter case is quite common.
So parsing and validation can be done in one-step. For example parsing the price
case looks like this.
| [| LC "price"; Int id; Decimal price |] ->
We Pattern Match and only if we have an Array with three elements and the second element can
successfully transformed into an int
and the third element can be turned into a Decimal
,
only then the case successfully matches. id
and price
are also int
and decimal
not option
.
|
|
Also the CLI
functions have the idea that they just return the new Map
. showProduct
is a operation that could fail, as the specified entry cannot exists. That’s why we handle
both cases here. We either print a message that the Product didn’t exists, or we print
the returned product
. Because we expect the new Map
state as a return value, but
showProduct
never changes Map
, we always just return db
(the input Map
).
|
|
Our DB.updateId
already handled the option
for us, that means we just can write
the updateName
and updatePrice
functions without thinking about option
. And still
everything works as expected without any failure!
|
|
In executeCommand
I use the option
type for signalling if we still continue or want
to stop. The idea is once again that we just return a new Map
after every command.
That Map
contains the new state. But once I return None
it marks an end. Here
you also see the mapping from the Command
to the actual functions. The Invalid
case for example doesn’t abort the program. We just print an error message and just return
db
unchanged. Only the Exit
command ends the program.
|
|
Near the end I simplified the whole program into two function. Parsing a string
to a Command
, and executing a Command
that returns us the next Map
. At this
level we just compose both operations into a single function. Now we have eval
.
eval
takes a Map
and an input string
, and will just return a new updated Map
for us. We already achieved a higher-level beyond error or option checking.
|
|
The only thing needed is the main program loop. We just print “Command: " to the terminal.
We read a string. Pass it to Cli.eval db
. This will Parse our string, do all kind
of checking and just returns us a eventually a new Map
. As long we get a new Map
.
we just call loop
again that recurs with the new Map
.
|
|
We create an immutable Map
as our starting database. With main storage
we finally
start our whole application loop.
Further Reading
Full Code
|
|