This article is part of a series aimed at helping TypeScript users unfamiliar with functional programming approach it by examining the fp-ts
library.
Someone new to functional programming who read the previous article on pipe, flow and currying might wonder how pipelines are useful with real life scenarios. For example what happens if we have to deal with something which sometimes returns no value? To address this problem we will introduce the Option
monad.
To make this as easy to digest as possible, we will avoid any subsequent mention of the dreaded monads, and simply describe the Option
in a practical way. In fact you can just think of Option
as a module. The Option
module defines its own type, which is unsurprisingly named Option
.
Discriminated Unions in TypeScript
If you are reading this article it probably means you are familiar with how types can be built from other types in TypeScript, in particular using object types and with the operators for unions |
.
For example, from a type A
we can create a new object type objectA
which takes the form:
{
_tag: 'A',
value: valueOfTypeA
}
I can create another type of an analogous form from type B
called objectB
:
{
_tag: 'B',
value: valueOfTypeB
}
I could then define a new type from these two C = objectA | objectB
. This means that an element of type C
will be of either type objectA
or objectB
.
Our new type C
is a demonstration of what is called a discriminated union. The "union" part of the term should be fairly obvious, given that it is a union of two types. The "discriminated" part of the term has to do with the _tag
property. An element of type objectA
can conceptually be thought as an element of type A
which has been tagged.
In TypeScript, discriminated unions can be implemented as unions tagged with a string field, and they are needed to implement the Option type. Read more about discriminated unions in the TypeScript documentation.
Defining Options
Defining the Option type
The Option
module can be imported directly
import * as O from "fp-ts/lib/Option";
As mentioned we can import the Option
type from this module. If we were to define it ourselves it would look like:
type Option<A> = None | Some<A>
This means that an element of type Option
will either be of type Some<A>
which looks like:
{ _tag: 'Some', value: someValueOfTypeA }
or of type None
which looks like:
{ _tag: 'None' }
This looks a lot like a discriminated union except one of the types is void
. The Some<A>
value can be thought of a value of type A
tagged with Some
, while None
is just the tag None
.
We can already get a sense of how this will be useful. Our first pass of our code might follow a naive approach which while in a big picture sense does what we want, in practice breaks down because some part of the process might not return a value. This would be a problem, given we're advocating functional programming where one wants continuity of input and output through a series of functions.
The Option
module remedies this problem by taking those situations where a return type is void
and turning it into an actual element of type None
. Otherwise when we do get a value, say of type A
, that value will be tagged with Some
and returned, or to be more accurate an element of type Some<A>
with the value inside will be returned.
Pushing a value into Option world
We can only take advantage of this Option
type if we have a way to somehow translate the values and methods in our naive code into an 'option-ized' version. Thankfully the Option
module provides what we need.
First things first, we'll see how we can take elements of some type and bring them into the world of Option
.
The first one is of
declare const of: <A>(a: A) => Option<A>
of
takes in a value and returns an option in a natural way
O.of(aValueOfTypeA) // returns { _tag: 'Some', value: aValueOfTypeA }
There is another method fromNullable
which has the same effect provided the value being entered isn't null
or undefined
.
O.fromNullable(aValueOfTypeA) // returns { _tag: 'Some', value: aValueOfTypeA }
However, if it is of a None type, the results are different
O.of(undefined) // returns { _tag: 'Some', value: undefined }
O.fromNullable(undefined) // returns { _tag: 'None' }
To generate a None Option one can simply use none
, for example:
O.none // returns { _tag: 'None' }
How to move around in Option world
There is nothing stopping one from using repeated applications of the some
function creating a kind of "nested" Option
.
For example:
const someOption1 = O.some(1) // { _tag: 'Some', value: 1 }
const someOption2 = O.some(someOption1) // { _tag: 'Some', value: { _tag: 'Some', value: 1 }}
This isn't particularly useful. If we wanted to get back down to a "lower level" we can use flatten
, like so:
const someOption2 = { _tag: 'Some', value: { _tag: 'Some', value: 1 } }
O.flatten(someOption) // returns { _tag: 'Some', value: 1 }
But it's not enough just to be able to turn the values we have into options. The functions found in our original code don't know about options so they are useless here. Worse yet there is still the issue that these functions might possibly not even give a value at all. Thankfully the Option
module gives us a way to "lift up" a function which so that we can deal with this problem.
map
is what is known as a higher order function, for the reason that it takes a function/s as an argument, and returns a function. It takes in a function of type (arg: A): B
and spits out a new function of type (arg: Option<A>): Option<B>
.
Let's give a concrete example:
We use the inbuilt fp-ts
tool, fromNullable
to create an item of type Option<string>
:
const optOfStr: O.Option<string> = O.some("asd") // { _tag: 'Some', value: 'asd' }
We define a function which takes in an item of type string and returns a value of type number:
const strLenPlus1:((str: string) => number) = ((str: string) => (str.length + 1))
strLenPlus1("asd") // returns 4
We "Option-ize" this new function with map
, creating a function which takes in an item of type Option<string>
and returns a value of type Option<number>
:
const optStrLenPlus1:((str: O.Option<string>) => O.Option<number>)
= O.map(strLenPlus1)
optStrLenPlus1(optOfStr) // returns { _tag: 'Some', value: 4 }
chain
is another tool provided by fp-ts
to create new versions of functions but which is more powerful than map
. It is also a higher order function, but the input is a function of the form (arg: A): Option<B>
and returns a function of the form (arg: Option<A>): Option<B>
.
Now lets give a concrete example of chain
:
If we start with a function that takes in a string and returns an Option<number>
const stringLengthOption = (str: string) => O.some(str.length)
stringLengthOption("asdf") // { _tag: 'Some', value: 4 }
We can now make a new function which takes in an Option<string>
and returns Option<number>
const stringOptionToNumberOption = O.chain(stringLengthOption)
const someStringOption = O.some("asdf") // { _tag: 'Some', value: 'asdf' }
stringOptionToNumberOption(someStringOption) // { _tag: 'Some', value: 4 }
Conclusion
So even in the case of where we aren't guaranteed values of a non-nullable type or even any value at all, we don't have to throw in the functional programming towel. The fp-ts
library gives us tools to enhance what TypeScript already gives us so that we don't have to break from the function to function pattern of our program. In future articles we'll see how the Option
module is just an introduction to the way fp-ts
helps us avoid these kind of problems.