Let us begin with a concrete problem and gradually work our way backwards into understanding why it would make sense to use them. We will be implementing a simple Option and Either Type along the way.
Wait… Why are you implementing your own Option types when there are existing libraries in the wild?
By implementing Option and Either types ourselves, our team had the opportunity to learn about its internals, rather than diving head-first into monads and functors. I certainly had a better understanding of how map or fold works because I could easily refer to the files within our code base!
Alternatively, if you are interested in existing implementations out there, check out:
Regardless of which implementation you use, I hope that this post gives you a better grasp of the concepts which are transferable across different implementations.
Problem: Parsing Data
Imagine that your app has fetched some data from an API and returned the JSON response below:
Task: Display the value of bedrooms in the UI
However, any of the properties below may or may not exist:
- If bedrooms.value does not exist, return zero
Define getBedrooms as a function
The code above helps illustrate a simple problem: having to null-check values when accessing properties. We have to do so every step of the way, potentially littering our code with many ifs.
Forgetting to do so will throw a run-time error.
Looking at this from another perspective, you could argue that the problem is that we are trying to access or operate on values that may or may not exist.
Cruising through nulls with Option Types
When you store a value in an Option type, it could either be a:
- Some, i.e. it is a non-null value
- None, i.e. null or undefined
Instead of operating on a value directly, wrapping it in an Option, making it a Some or None, allows us to pass it a function, f, which is invoked on the value if it is present.
Option Type Definition
The code above is a basic implementation of Option that will help us get along our merry way towards reducing complexity.
What does map, flatMap, and fold do?
There is an important but subtle difference between map and flatMap, and that is the type of function you pass to it.
The map function takes a function which transforms a value into another value.
f: U => V
maybeAValue.map(x => x * 2)
The flatMap function expects a function which returns a Some or None.
f: U => Option<V>
maybeAnID.flatMap(id => getSomethingWhichMayNotExist(id))
On the other hand, fold can be used to extract the value held within an Option type. When you fold an Option type, you are forced to acknowledge that the value within may or may not exist. This is why fold expects two functions – one for when it is a None and one for when it is a Some. For example:
ifEmpty — evaluated if the value is a None
Option(null).fold(() => 'Empty!', x => x); // 'Empty!'
f — applied to the value within if it is a Some
Option('Something').fold(() => 'Empty!', x => x); // 'Something'
The illustration below is my attempt at explaining the differences, inspired by Functors, Applicatives and Monads in Pictures.
Let us put Option types to use by refactoring getBedrooms!
Refactor getBedrooms to use Option Types
This is our original code from earlier with all the null checks
Now that we know about Options, we can use them to wrap data.listing, which can be null.
In getBedrooms above, we use Option to wrap data.listing, which could be null.
If listing does exist, we access listing.features, which again could be null. We use flatMap to keep chaining our accessor functions until we get to bedrooms.
We finally retrieve the value by fold-ing by providing it with two functions:
- () => 0, used if we have a None
- v => v, used if we have a Some
👍🏻 Signals the fact that a value could be null. If you receive an Option as a value and wish to operate on it, you would have to fold it. It is a nice way of enforcing the consumer to handle both a None or Some if it wishes to use the value.
👍🏻 Makes it easy to chain functions and not worry about null checking at all. This can be pretty powerful once you get used to the concept. It really encourages you to think of problems from a different perspective.
👎🏻 Can be verbose. Looking at the initial solution without Option types, you could argue that it has less lines.
👎🏻 We cannot fully harness the power of Option types without typing our values. Part of the definition of map or flatMap requires the arguments passed in to conform to a certain signature, e.g.
flatMap: U => Option<V>.
Our team has been been taking incremental steps to introducing types to our codebase using FlowType, a type checker. This has allowed us to slowly spread types across the codebase as we become more comfortable with them and improve our type signatures.
- Familiarity with types in general
- Expressing our intent and providing more context in our code
With FlowType we are able to statically check our Option Types, which actually turns a 👎🏻 into a 👍🏻!
👎🏻 An additional layer of abstraction to solve a problem.
Every abstraction incurs a mental cost to us as developers. You could argue that there are less complex ways of tackling this problem, e.g.
I believe that Option types, and any other Algebraic Data type, are not just another abstraction. They are concepts which are applied in functional programming to help eliminate complexity by increasing readability and the ability to reason about your code. Over time, just like understanding traditional code design patterns, using Option types will change how you go about solving a problem, as you might see recurring patterns and consider using them to reduce complexity.