Categories
Tech

F# Friday – The Option Type, Part 2

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”

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.