United Kingdom: +44 (0)208 088 8978

Working with optional data in Elmish

Isaac shows us an elegant way to deal with optional data within List comprehensions.

We're hiring Software Developers

Click here to find out more

Today, I want to show a nice trick for dealing with optional data when working in Elmish-style code safely and succinctly. Elmish code is common for it's use of list comprehensions, which are extremely useful for creating UIs with conditional logic e.g.

let view = [
    text "Checkout"

    // other UI elements...

    if model.IsHighOrderValue then
        banner "Are you sure you wish to proceed? This is a high value order!"
    button "Ok"
]

Here, we're using if to conditionally show a banner based on the state of our model. But list comprehensions are a feature in F# that you can use in all sorts of places. Considering the following code sample, which returns all the available values of an Address as a single list of strings:

let address = {| Line1 = "1 The Street"; Line2 = Some "The Town"; City = "The Big City"; Country = "UK" |}

let addressParts = [
    address.Line1
    match address.Line2 with
    | Some line2 -> line2
    | None -> ()
    address.City
    address.Country
]

// [ "1 The Street"; "The Town"; "The Big City"; "UK" ]

We're using F#5's implicit yield feature here to yield back the values. You could add the yield keyword on each statement as well if desired.

The problem here is that yielding Line2 is quite ugly - you can't just yield back the optional value directly (the other values are yielding back plain strings), and although in this example it's only a single value, imagine if there were a few optionals in the comprehension - the signal/noise ratio would start to equalise (in fact, there's probably more noise than signal for Address.Line2 in this example).

Thankfully, there are a few ways around this. Firstly, you can lift all values into options and then choose just the "some" values:

let addressParts =
     List.choose id [
        Some address.Line1
        address.Line2
        Some address.City
        Some address.Country
    ]

In other words, we're converting all values into Options so that we can treat the "real" optional values normally, and then use List.choose to "discard" any None values. However, this adds extra noise elsewhere:

  • The extra call to List.choose
  • The change in syntax layout with extra indentation
  • Having to add Some for the "happy path" values

It's also less performant as you're wrapping and unwrapping lots of values in options unnecessarily, as well as needing to pass through the list an extra time to strip out potentially a single None value.

Another option is to replace match with if and "unsafely" unwrap the option:

let addressParts = [
    address.Line1
    if address.Line2.IsSome then address.Line2.Value // unsafe unwrap
    address.City
    address.Country
]

One of the nice things about if in F# is that when used in statement form (and I think of yields as statements in this case), then the compiler will automatically add an else () branch for you. So the noise has been reduced here, but we're instead left with the use of the somewhat ugly .IsSome and .Value members - the latter is from a type-system point of view unsafe, and in general I would say that this is not a satisfying solution. We also can't wrap this boilerplate pattern into a function - it doesn't work from a type-system point of view.

Lists and Options

A solution that I prefer is to step back for a minute and to think about the relationship Lists and Options, and how you can actually model Optional data as a list:

Option List
No data present None [] // empty list
A value is present Some x [ x ] // single value list

In fact, it turns out that F# has a built-in combinator function in the Option module to convert from Options to Lists: Option.toList that follows the pattern in the table above.

Once you think of it like this, you can also start to see List functions through the prism of optional data:

  • List.map / Option.map
  • List.concat and List.collect / Option.bind
  • List.filter / Option.filter

etc.

As a side note, whilst there is no built-in combinator for going from Lists back to Options (what would you do for a list which had more than a single value in it) but you could write your own which e.g. dropped subsequent values if they existed in the list. In this way, you can harness many List module functions for Optional data.

Back to our challenge at hand, why is this relevant? Because F# also has a keyword for yielding multiple values at once from a list within comprehensions, called yield!:

let foo = [ 1 .. 5]
let bar = [
   yield! foo
   6
   7
]
// [ 1;2;3;4;5;6;7 ]

yield! works with Lists and Arrays "out of the box", but not options. However, armed with this and our new-found knowledge of Options and Lists, we can now yield! optional data:

let addressParts3 = [
    address.Line1
    yield! address.Line2 |> Option.toList
    address.City
    address.Country
]

To me, this is the most satisfying (or least unsatisfying) solution to this - any noise is localised to the "exceptional" data (in this case, the single optional value), it's not disruptive from a syntax / layout point of view, and it uses a built-in keyword and function. And although you can't remove the use of yield!, you could make an shorter alias or even an operator for this - as always with custom operators, please use carefully!.

let opt = Option.toList
let (!!) = Option.toList

let addressParts3 = [
    yield! address.Line2 |> Option.toList
    yield! opt address.Line2
    yield! (!!) address.Line2
]

Summary

List comprehensions are a useful and powerful tool for F# developers. They are extremely popular within Elmish code but can be used in many situations. By seeing how to convert between options and lists of data, we can use F#'s built-in yield! operator to yield back optional data succinctly within such comprehensions, improving the quality of our code.