Last week we introduced the Option
type as a way of representing when a function may or may not return a value.
A Collection of One
Another way to think about an Option
is as a collection that contains no more than one element. Consequently, the Option module provides some collection-like semantics, e.g., fold
, map
, exists
, and even filter
in F# 4.0. This allows us to chain operations together without having to check at each step whether the value is Some
or None
. We can put that off until the end of the sequence of operations and only do the check once. That way, the algorithm is more readable; it’s not cluttered with a bunch of if-else
noise.
Say that you have a User
type:
type User(id : string, name : string) = member x.Id = id member x.Name = name
Let’s say that your system is a website. In the top, right corner of the site, you want to display the name of the user who is currently logged in. You need a function that returns the user currently signed in:
let authenticatedUser : unit -> User option = // ...
Now why would this function return an Option
? Well, the user browsing your site may not have logged in at this point. If that’s the case, then the current user is None
. So then, the code to get the name of the current user is this:
let nameOpt = authenticatedUser() |> Option.map (fun u -> u.Name) // val nameOpt : string option
Notice how the type of nameOpt
is string option
. In other words, assuming that authenticatedUser()
returns a Some<User>
, F# knows that the User.Name
property is a string
. Nevertheless it propagates the uncertainty, if you will, along through the map
operation. Any operations you do on an Option
only happen if the Option
is a Some
. Otherwise, F# happily ignores the operation and continues to propagate the None
.
OK, so now that we have a final result Option
, what do we do with it? That’s where defaultArg
comes in:
let name = defaultArg nameOpt "Guest" // val name : string
If nameOpt
is a Some
, name
is the value in the Some
. If nameOpt
is a None
, then name
is "Guest"
. You can test both cases in the F# REPL. To see it work in the case of a Some
:
let authenticatedUser : unit -> User option = fun () -> Some(User("bcollins", "Brad Collins")) let nameOpt = authenticatedUser() |> Option.map (fun u -> u.Name) // val nameOpt : string option = Some "Brad Collins" let name = defaultArg nameOpt "Guest" // val name : string = "Brad Collins"
And then to see it with a None
:
let authenticatedUser : unit -> User option = fun () -> None let nameOpt = authenticatedUser() |> Option.map (fun u -> u.Name) // val nameOpt : string option = None let name = defaultArg nameOpt "Guest" // val name : string = "Guest"
Unraveling the Options
Remember how the collections modules define the collect
function? For instance, this is the List.collect()
function:
List.collect : ('T -> 'U list) -> 'T list -> 'U list
It’s like map
, but collect
takes a transformation function that takes an element and returns a list of values rather than just a single value. Then instead of returning a list of lists, collect
flattens them into a single list.
Option
has a similar function: bind
. The bind operation takes a function that returns an Option
and, instead of returning a nested Option
(i.e., an Option<Option<'U>>
, it just returns an Option<'U>
:
Option.bind : ('T -> 'U option) -> 'T option -> 'U option
Maybe instead of having an authenticatedUser()
function, your app just has a Session
property. To get the name of the current user, you have to walk the property tree from the application to the session to the user and then finally the name. App.Session
is an Option
to indicate that there may be no valid session. Session.User
is an Option
to indicate that there may be no user signed in. Finally, maybe we don’t even force a user to have a name, just a user ID:
type User(id : string, name : string option) = new(id : string) = User(id, None) member x.Id = id member x.Name = name type Session(user : User option) = new() = Session(None) member x.User = user type App(session : Session option) = new() = App(None) member x.Session = session
You walk the property tree like this:
let nameOpt = app.Session |> Option.bind (fun s -> s.User) |> Option.bind (fun u -> u.Name) let name = defaultArg nameOpt "Guest"
Now if any property is None
, from Session
to User
to Name
, nameOpt
is None
, and name
is "Guest"
.
One reply on “F# Friday – The Option Type, Part 2”
[…] F# Friday – The Option Type, Part 2 – Brad Collins […]