The Power of F# Discriminated Unions
A few months ago I got back into Clojure for my day job at Cognician. It’s a very interesting experience to use multiple contrasting programming languages on a regular basis. At this stage mine being Clojure and F#. You truly experience the unique strengths and weaknesses of each language anew every day.
One of the things I love about Clojure is how easy it is to have a collection with different types in, and the power that gives you to seamlessly transport and transform data. This will be true to a degree for any untyped language, although the cohesion of Clojure’s data structures go much deeper than most other languages. Every now and then I still run into some of the trenches of F#’s type system, where I have to fight a bit with it to get it to do what I want it to. This will be even more true of other typed languages I believe, since I consider F#’s quite good. Although F# already feels quite dynamic thanks to it’s pretty good type inference, at moments like these I can’t help to ponder ways in which I can use F# so that it behaves more like a dynamic language? On the Clojure side devs are asking similar questions, but just how to get some of the benefits of typed languages, that’s why we have things like clojure.spec, Schema and Typed Clojure.
All the aforementioned lead me to recently realize the power of F#’s discriminated unions, hidden in plain sight. Consider the following discriminated union and function:
type RestRes<'a> =
| GetAll of (unit -> 'a list)
| Create of ('a -> 'a)
| GetById of (int64 -> 'a option)
| IsExists of (int64 -> bool)
| Update of ('a -> 'a)
| UpdateById of (int64 -> 'a -> 'a)
| Delete of (int64 -> unit)
let inline rest resourceName (resources: RestRes< 'a> list) =
On first sight it doesn’t seem like much. But think carefully about it again, and you will realize that this allows a RestRes< ‘a> collection to contain any of it’s types. Okay, so what you say, you can achieve the same thing with inheritance. Well, discriminated unions are more flexible than inheritance because you can add the same type to multiple discriminated unions. It’s also safer, because no functionality from the parent leaks into the children. It’s only a structure to integrate various types, not their behavior.
The discriminated union can seamlessly cloak your types when you don’t need them, and then you can use pattern matching (destructuring in Clojure) to uncloak them when you want to filter for specific types:
((match method with
| GetAll(getAll) -> ...
| Create(create) -> ...
| Update(update) -> ...
| _ -> ...),
match method with
| GetById(getById) -> ...
| UpdateById(updateById) -> ...
| Delete(deleteById) -> ...
| _ -> ...)
So in the above example, I’m using match expressions to create a tuple with two members; one for by-Id methods and one for the others. But, the nice thing is, it allows me to call the function with code that looks like this:
You can neatly create a list with any number of different rest method types. The rest function will take care of filtering the list and effortlessly pick the types it needs to operate on. Yet nowhere do you have to concern yourself with the discriminated union’s type when calling the rest function. The union quietly works in the background to allow you to use different types without realizing it. The F# list now behaves much more like it’s Clojure counterpart, than a usual list of a single type.
So now imagine you have a domain model with lots of different records, tuples and other types. Say you want a function that needs to be able operate on or return a collection with mixed domain types. Now you can easily use a DomainModel discriminated union to achieve this, without restricting the types in the domain model to belong to any number of type categories.