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
andList.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.