Option

Maybe there is a value

Option represents a value that maybe is present. Is roughly equivalent to const a: T | undefined

for example, accessing the first element of an array can be undefined

const arr: number[] = [];

const firstElement = arr[0]; // undefined

before manipulating firstElement checking for undefined is needed

let firstElementTimesTwo;
if (firstElement !== undefined) {
  firstElementTimesTwo = firstElement * 2;
}

luckily we can use one of the function form Array to do the same thing

import * as O from "fp-ts/lib/Option";
import * as A from "fp-ts/lib/Array";

const safeFirstElement: O.Option<number> = A.head(arr);

What is Option

For fun let's create the same function. An easy way to implement is to check for the array length and return the result based on that.

At the start of this chapter, I said that Option is roughly equivalent to const a: T | undefined? This because Option is represented by O.some<A> | O.none

For fun let's implement the function ourselves, in input, we pass the array and then we return an option. How we can return an Option? By returning some if the value is present, otherwise none. Knowing this we can say that type Option<A> = Some<A> | None .

With O.some(1) the Option is created with said value, this is a function because the value can change.

import * as O from "fp-ts/lib/Option";

function safeHead<A>(arr: A): O.Option<A> {
  return arr.length > 0 ? O.some(arr[0]) : O.none;
}

With O.none the Option is created without the value so only a constant is needed.

if you log O.some(1) you will get {"_tag":"Some","value":1} while O.none correspond to {"_tag":"None"} None can possibly be only that object hence the constant.

This is the internal representation of the Data Types.

With this information in mind, we can write our function for safeHead

function safeHead<T>(arr: T[]): O.Option<T> {
  return arr.length === 0 ? : O.none : O.some(arr[0]);
}

As you can see thisis how it's implemented inside fp-ts

There are a lot of functions that are already implemented for common operation, one goal of this book is to help discover said operations.

Map

Now we have an Option<number> which means that maybe there is a value maybe not. How can we manipulate the value? By using the function named map

import { pipe } from "fp-ts/lib/pipeable";
import * as O from "fp-ts/lib/Option";
import * as A from "fp-ts/lib/Array";

const arr: number[] = [];
const safeFirstElement: O.Option<number> = A.head(arr);

const firstElementTimesTwo = pipe(
  safeFirstElement,
  O.map(value => value * 2)
);

Note that with map the function is applied only if the value is present.

For now, the difference is not that great, the biggest one is the use of const instead of let.

Chain

Moving on let's say that we need to divide this resulting number by 0. Because is an operation with a special case we use another Type Class called chain that enables us to change the "branch" of the Option

const firstElementTimesTwoDividedByZero = pipe(
  firstElementTimesTwo,
  O.chain(n => (n === 0 ? O.none : O.some(1 / n)))
);

Why we can't use map? The reason is that in this case, the function may fail so we return an Option, with map we would be with Option<Option<A>> and that's not good, chain is a function that "flattens" the result.

chain and flatMap are two functions that can be derived from one another and are not the same function with different names. More info can be found here

Filter

Now let's say that the result needs to be greater than one. We can use filter

This function accepts a predicate, based on the result of the predicate "keeps" the value of the Option or "discards" it in the None

const firstElementTimesTwoDividedByZeroGreaterThanOne = pipe(
  firstElementTimesTwoDividedByZero,
  O.filter(n => n > 1)
);

You can obtain the same result using chain

const firstElementTimesTwoDividedByZeroGreaterThanOneWithChain = pipe(
  firstElementTimesTwoDividedByZero,
  O.chain(n => (n > 1 ? O.some(n) : O.none))
);

Final Example

You noticed that I used constants to better illustrate the various operation in isolation. Usually, fp-ts is used in a single pipe to better understand the various operations.

import { pipe } from "fp-ts/lib/pipeable";
import * as O from "fp-ts/lib/Option";
import * as A from "fp-ts/lib/Array";

function ComputeWithFpts(array: number[]): string {
  return pipe(
    A.head(array),
    O.map(n => n * 2),
    O.chain(n => (n === 0 ? O.none : O.some(1 / n))),
    O.filter(n => n > 1),
    O.fold(
      () => "ko",
      (result) => `the result is: ${result}`
    )
  );
}

console.log(ComputeWithFpts([1]));

for comparison here is the corresponding example without using fp-ts

function ComputeTheOldWay(array: number[]): string {
  const firstElement = array[0];
  if (firstElement === undefined) return "ko";
  const firstElementTimesTwo = firstElement * 2;
  if (firstElementTimesTwo === 0) return "ko";
  const division = 1 / firstElementTimesTwo;
  if (division <= 1) return "ko";
  const result = division;
  return renderSuccess(name, `the result is: ${result}`);
}

console.log(ComputeTheOldWay([1])

All the code that appears in this page you can find it here

Last updated