F#: There was always an Option
I’ve been messing around with F# lately. I’ve glanced at ML/OCaml/F# code before, but this is my first experience really working in an ML descendant. So far, I’m really liking what I see. Today I encountered something that I found interesting enough to write a few paragraphs about.
Options
If you’re not familiar with what an Option type is, take a look at the Wikipedia page. I’ll attempt to describe it very briefly as:
Option: A type that wraps a value of another type, where that value may or may not be present.
Proponents would say that Options are a better alternative to null when you desire to express a value which may not exist, because they force you to deal with that fact at compile time.
There is a LOT more to say about Options, but this isn’t a blog post about why they exist. This is a blog post about an interesting design choice that appears in a language where library creators have always been able to assume they are present.
Filtering, Transforming, and Choosing with Options
The Option type isn’t unique to F#. Java, for example, has Optional. However, whereas Java’s Optional is a feature that was added to a language that had already been around for a long time (it was added in Java 1.8), F#’s option type has been in the language since its inception. In fact, my second edition of ML for the Working Programmer, published in 1996, lists an option type as part of ML’s standard library. In other words, F#’s ancestor had options since at least 1996 (and probably long before).
This means that all F# code in existence was written for a platform where option existed from day one. This has led to library functions that would never have occurred to someone who’s been writing Java for over two decades (me).
Java Example
All programming languages that I’ve used have had some sort of collection abstraction, and they’ve all had at least the two following operations defined for collections: filter and transform. Sure, these operations often go by different names (grep, remove-if-not, map, …) – but they are always there.
The filter function takes two arguments: a collection and a unary predicate. It returns a new collection containing only the elements of the original collection for which the predicate returns true.
The transform function takes two arguments: a collection and a unary function. It returns a new collection whose elements are the result of applying the function to each element in the original collection.
For example: let’s say that given a collection of integers, you wish to take only the even ones and divide them by three, resulting in a collection of floating point numbers. In Java, that could be accomplished as follows:
$> jshell
jshell> var ints = IntStream.rangeClosed(1,10).mapToObj(Integer::valueOf).collect(Collectors.toList())
ints ==> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
jshell> ints.get(0).getClass()
$27 ==> class java.lang.Integer
jshell> var floats = ints.stream().filter(n -> n % 2 == 0).map(i->i/3.0).collect(Collectors.toList())
floats ==> [0.6666666666666666, 1.3333333333333333, 2.0, 2.6 ... 66665, 3.3333333333333335]
jshell> floats.get(0).getClass()
$29 ==> class java.lang.Double
Most of that code is boilerplate that lets me show you the types of the expressions (to convince you that I started with ints and ended up with floats). The really important part is:
var floats = ints.stream().filter(n -> n % 2 == 0).map(i->i/3.0).collect(Collectors.toList())
Note the successive calls to filter and map (a.k.a.: transform). This is the tried and true method of solving this type of problem that I’ve been using for over two decades.
F# Example
In F#, the same example uses options to merge the filter and transform operations into the same function! Perhaps to you this may seem like a little thing… but when I saw it, I was taken aback. It’s always interesting to see a new way of doing something you’ve been doing the same way for your entire career:
$> dotnet fsi
> List.choose;;
val it: (('a -> 'b option) -> 'a list -> 'b list)
> let ints = [1..10];;
val ints: int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
> ints |> List.choose(fun i -> if i % 2 = 0 then Some(float i / 3.0) else None);;
val it: float list =
[0.6666666667; 1.333333333; 2.0; 2.666666667; 3.333333333]
Look at the signature of List.choose, the method that both “filters” and “transforms”:
('a -> 'b option) -> 'a list -> 'b list
List.choose, as its only parameter, takes a function that maps a value of type ‘a to an option of type ‘b. That function can do two things:
- Convert an instance of ‘a to an instance of ‘b
- Indicate inclusion-in or exclusion-from the result via an option
Given this argument, List.choose then builds a function that can be applied to an input collection (ints in the example) to generate an output collection that has undergone both a “filter” and a “transform”!
Okay, in the spirit of full disclosure: sure, F# also has List.filter and List.map (a.k.a.: transform). Still, I still find it very interesting that the option type allowed them to include List.choose in the standard library. Perhaps it is even more interesting that they opted to do so.
-
2025-11-28
Fsharp -
2024-06-25
Spring @Cacheable -
2023-12-03
Lisp Macros -
2022-12-31
Happy New Year