One type of discriminated union that is commonly used in F# is the
Option type. The Option type is used to
represent a value that may be present or absent. It is defined as
follows:
type Option<'T> =
| Some of 'T
| None
The Option type is generic; we can use with any value
type but it’s more like a wrapper around a value, rather than a value
itself. Let’s see how it works:
> let aNumber = 5;; // regular int
val aNumber: int = 5
> let maybeNumber = Some 10;; // optional int that is present
val maybeNumber: int option = Some 10
> let missingNumber : int option = None;; // optional int that is absent
val missingNumber: int option = None
It’s important to note that maybeNumber and
missingNumber are of type int option, which is
different from aNumber, which is of type int.
This means you can’t do this:
> let sum = aNumber + maybeNumber;; // error: type mismatch
Instead, we have to pattern match on the Option type to
access the value inside it:1
> let sum =
- match maybeNumber with
- | Some number -> $"sum = {aNumber + number}"
- | None -> "Cannot perform operation";;
val sum: string = "sum = 15"
To help work with Option types, F# provides some useful
functions in the Option module, two of which include Option.map
and Option.bind.
To see how Option.map works, let first examine it’s type
signature:
Option.map : ('a -> 'b) -> 'a option -> 'b option
What this means is that Option.map takes:
a function that transforms a value of type
'a -> 'ban
Optionof type'a option
and applies that function on 'a option to return an
Option of type 'b option. For example, let’s
say we have a function that doubles an integer:
> let double x = x * 2;;
val double: x: int -> int
If we try to pass maybeNumber, which is of type
int option, directly to double, which is
expecting an int, we get a type error:
maybeNumber |> double;;
---------------^^^^^^
error FS0001: Type mismatch. Expecting a
'int option -> 'a'
but given a
'int -> int'
The type 'int' does not match the type 'int option'
We can instead use Option.map to apply the function
double on maybeNumber:
> maybeNumber |> Option.map double;;
val it: int option = Some 20
Option.map can also gracefully handle the case when the
Option is None:
> missingNumber |> Option.map double;;
val it: int option = None
In the example above, our function double took an
int and returned an int, but
Option.map transformed it to work with Option
types, taking an int option and returning an
int option. The way I think about it is that
Option.map “unwraps” maybeNumber to a normal
int, applies the function, and then “rewraps” the return
value with the Option type.
But what if we have a function that returns an Option
type? For example, let’s say we have the following function to check
whether the divisor is zero:
> let tenDividedBy x =
- match x with
- | 0 -> None
- | _ -> Some (10 / x);;
val tenDividedBy: x: int -> int option
Like before, with our double function, we can’t directly
pass maybeNumber, which is of type int option,
to tenDividedBy which is expecting an int:
> maybeNumber |> tenDividedBy;;
maybeNumber |> tenDividedBy;;
---------------^^^^^^^^^^^^
error FS0001: Type mismatch. Expecting a
'int option -> 'a'
but given a
'int -> int option'
The type 'int' does not match the type 'int option'
However, we can’t use Option.map here either:
> maybeNumber |> Option.map tenDividedBy;;
val it: int option option = Some (Some 1)
as it returns a nested Option type of
int option option, which is not what we want. (Using my
thinking from above, Option.map “unwrapped”
maybeNumber to an int, applied the function
which returned an Option type, and then “wrapped” that in
another Option.)
Instead, we can use Option.bind, which has the following
type signature:
Option.bind : ('a -> 'b option) -> 'a option -> 'b option
The only difference between Option.map and
Option.bind is that the function passed to
Option.bind must return an Option type. This
allows Option.bind to “unwrap” the nested
Option type that we got from using Option.map
(i.e. Option.bind doesn’t “rewrap” what was returned from
the transform function). If I had to guess, bind refers
binding the final type to what was returned from the function, but don’t
quote me on that! So now we can do this:
> maybeNumber |> Option.bind tenDividedBy;;
val it: int option = Some 1
> missingNumber |> Option.bind tenDividedBy;;
val it: int option = None
> (Some 0) |> Option.bind tenDividedBy;;
val it: int option = None
You can also use the
Option.getfunction to directly extract the value from anOption(i.e.let sum = aNumber + Option.get maybeNumber), but this will throw an exception if theOptionisNone, so it’s generally safer to use pattern matching or the functions in theOptionmodule.↩︎