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.,
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
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
Say that you have a
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
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
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
nameOpt is a
name is the value in the
nameOpt is a
"Guest". You can test both cases in the F# REPL. To see it work in the case of a
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
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 : ('T -> 'U list) -> 'T list -> 'U list
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.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