F# handling the option type
Intro
The steady march towards learning F# continues. This time lets look at the option type, what it is, how it compares to nullables in C# and the basics of how you are supposed to deal with it. I originally started this post because I was dealing with option in a lot of my exploratory code, and the things I’ve read online about options implied they were special in some ways, but the further through I got the more I realise they are really just the F# version of the Nullable
type in C#, its how they are handled functionally thats special.
First the C# Nullable type
The closest relative to the option type in C# world is the Nullable<T>
type. Which allows you to declare something thats not assignable to Null (e.g. an int
) as nullable, and has properties that declare whether the variable is assigned a value or not, and gives you access to the value. For example:
*Prints nothing*
This piece of code will do absolutely nothing, because it declares the Nullable<int>
“test” with the shorthand ?
operator. It then proceeds not to assign anything to test, and to check its HasValue
property to see if its Null
. If it wasn’t Null
it would then print its value to the screen. Now we could of course not do this null check like so:
System.InvalidOperationException: 'Nullable object must have a value.'
Of course, this code will throw an exception, which is one step further than the first code that did nothing. Finally, just for completeness:
1
This prints the value 1, because thats whats been assigned to the nullable int. So how does this relate to F#?
On to F# options
Well, functional languages don’t tend to work with nulls, instead they deal with something called a maybe
or in F# (and OCAML) world option
. In F# you declare something as an option type by prefixing it with the words Some
for a value or None
for no value. For example:
Some(41)
What this piece of code is effectively saying is that x may contain an integer value of 41, BUT consumers of this variable need to make sure they handle the case where it doesn’t exist. Fortunately for our example case above, theres an automatic conversion to a string for the option type and we got “Some(41)” printed to the console.
Just to show the None
case:
*Prints nothing*
We’ve assigned None, and we got nothing printed, as you’d expect as long as the string conversions works correctly.
This is almost like for like how Nullable
works in C#, except F# explicitly prints Some
when converting to a string, and C# doesn’t. So.. if this is the same as a Nullable type in C# it follows that we can’t pass the variable directly to an int parameter:
And that turns out to be true, the compiler has rejected this code because the type option
and int
are incompatible for the +
operator. So how should we handle an optional type then?
The old (bad) nullable style handling
Of course, we could handle this case the same way as we do with Nullable
in C#. Option provides the methods
IsSome
IsNone
Value
These work in much the same way as HasValue
and Value
, and calling the Value
function without first checking if there is a value will result in an exception.
You could work away using these, writing if statements just like in imperative languages, but because this is the functional world, we don’t have to do that, instead we can use some of the built in language features to assist us to work in a way where exceptions can’t really happen, and None
values are checked at compile time.
Pattern matching
One of the really powerful features in F# is pattern matching. Its pretty useful for handling option
, its sort of a really concise switch statement. We can change our add function from above that failed to compile to correctly handle our option:
50
This time, we’ve handled that option type by creating a function called addOption
, all that function does is decide how to handle the logic of adding the two numbers together, either with a default value for the None
case, or with the actual value. The pattern matching syntax looks a little weird at first, but you get used to it, its basically match <variable> with |
where |
is the separator for each case. Within the first case we match x with None
, this is our condition where x isn’t set, so we declare it as 0 + y
, which has no effect. In our second case, we say Some x
which checks that x has a value, and then we use x as a standard integer value in the corresponding function.
This is slightly different to how we’d have handled this in C#, you wouldn’t normally reach for the switch statement in this situation. However in functional land, functions, and the match statement are so easy to create, small and concise that this code makes a lot more sense. Its also quite reusable now, you can use this for adding any optional int
, and avoid the possibility of exceptions due to improper handling of the option.
Of course we can also refactor this code into a higher order function, and make it much more reusable.
The higher order function version
Higher order functions are functions that take another function and operate over that function and associated data. For C# developers probably the easiest example is a Lambda Select
statement, e.g.
The select function, is equivalent to F# map, it takes the items
and applies a function x => x.ToString()
to each item. The important thing to understand here is that x => x.ToString()
is just a function we define and are passing to the Select
function, the Select
function itself will just consist of the functionality to apply the passed function to the items array. So Select
is a higher order function, it doesn’t do much by itself, but it facilitates doing many different things to our items collection just by taking that function as an argument.
Back to our F# then, lets assume we want to multiply our optional numbers as well. We know that for an addition our Identity (i.e. the empty case if you remember about monoids) 0, as any value + 0
will just return the original value. For multiplication, our Identity is 1 because 1 multiplied by any value just returns the original value too. So we need to pass in our identity, we also need to pass in the function for the operation (either + or -):
"50"
"82"
- So in this code we’ve renamed
addOption
tooptionOperation
and added two preceeding arguments,identity
andfunc
.identity
takes our default value for an operation for when our value is null.func
is our operation. We then use pretty much the same code asaddOption
, but we replace the + with func before the two argumentsfunc x y
and in ourNone
case we pass the identity. - These two lines are where the magic happens, we partially apply our
optionOperation
function for add and multiply. Foradd
we pass 0 as our Identity and we wrap the plus infix operator in brackets(+)
which turns it into a normal function that can be passed as an argument. We do exactly the same for the multiply, but with 1 and(*)
. - Finally in 3 we call our
add
andmultiply
functions (which are now functions in their own right), and pipe them through to be printed.
Our add and multiply function are also monoids
This is quite a neat solution, and it means we can reuse our option logic for different types too if we want. The optionOperation
can really run on any type now, with an identity and a function to apply. Lets append a function that concatenates two strings:
"I have been concatenated"
Null should not be used, if its empty, its an option
An important major distinction between F# and C# is that nothing should really be assigned NULL
. Instead option
is used, for everything, if it might not exist it is an option, and the compiler mostly forces you to handle the empty case, although as discussed not if you revert to the functions on option itself. In contrast in C# only types that can’t inherently be null can be dealt with explicitly using the Nullable<T>
type. Although C# 8 is about to add an option to change that.
For an example of F# allowing the use of option everywhere, heres a Tuple:
"Some((1, 2, 3, Fred))"
And we get the printed “Some” value as expected. And if you wanted to use the value within the tuple, you can pattern match just like with standard types. e.g.
"Some((1, 2, 3, Fred))"
"True"
"False"
Note: Here we use _ in our pattern match for the tuple
which denotes that we don’t care about that particular value.
Built in helper functions
option
also has its own set of helper functions in the Option module. These assist in the processing of option types much like the standard list operations a couple of examples are:
Option.map
applies a function to the option where the value exists:
Some "1"
null
Interestingly this prints
false
true
Option.get
returns the value stored in the option.
1
This prints 1 then throws an exception, presumably because its just calling x.Value inside that method, similarly to the map, this is not really ideal behaviour and I think I’d rather use a specific pattern match.
defaultArg
Another particular function of interest is defaultArg
, which gets you the value or default for the option you pass.
"0"
"1"
There are a whole bunch of other method types available, I won’t list them all, but if you use them, its probably worth working out their behaviour for the None
case. Its more functional to avoid nulls and exceptions, and with the tools at your disposal to do this, theres not really a reason not to.
Summary
After all this, they’re a lot like nullables really aren’t they. F# Just provides much better tools to handle them, as long as you avoid the exception causing/null returning functions.