HomeAbout UsNews, company reports and blogBlogTechIntroduction to fp-ts – Part 1

Introduction to fp-ts – Part 1

Article by Mark Gaylard
Published on Read in 18 mins

Functional programming is well established at REA in Scala, but less so in Typescript. While Scala has language features built in that make doing FP easier, we still rely on libraries to fully take advantage of the things FP can help us with. Typescript is similar, and although it has less built-in language support, we can use a library to provide the pieces we need to do FP in Typescript. One such library is fp-ts. This post will talk about some of the things fp-ts does for us and how it helps us write better, safer code in Typescript. We will start with things to help us with null and undefined, then look at synchronous error handling. The next post will cover asynchronous functions and error handling.

flow and pipe

Function composition is an important concept in FP. It is the idea that small, simple functions can be composed together to create larger, more sophisticated functions. fp-ts has a couple of helper functions that let us do this – flow and pipe.

flow

The documentation for flow says it “performs left-to-right function composition”. Let’s dig into that.

Let’s say we have some functions:

If we wanted to call these three functions in order – clean, len, double – then it might look something like this:

To read this, you have to read the code inside out which isn’t great. flow creates a function that calls the given functions in order. eg:

This is called function composition. It has composed the three functions into one. It takes the output of each function and passes it as the argument to the next. You would call it like this:

flow has some restrictions on the functions that can be passed to it arguments. The first function can have any number of arguments, the rest must take only a single argument (ie: have an arity of one – unary).

pipe

The documentation for pipe says it “pipes the value of an expression into a pipeline of functions”. What does that mean though?

We’ll reuse the same functions as before:

The difference between pipe and flow is that the pipe takes a value (or an expression, since every expression returns a value) as the first argument. You would call it like this:

The initial value is passed as the argument to first function. The output of that function is then passed as the argument of the next function and so on, in the same way as flow.

One thing that is important to note is that the functions passed to pipe need to all be unary.

Which one is better? Which should you use?

flow is more ‘FP’, but when we were learning fp-ts, we found pipe a bit easier to understand, but it doesn’t really matter too much as they perform similar functions. In our code, the majority of the time we will use pipe over flow.

A benefit to using pipe is that type inference works as you would expect, meaning you don’t need to explicitly add argument types to functions within pipe. This isn’t always the case with flow.

Option – Replacing null

We all know null and undefined are a big cause of runtime errors. Typescript does a pretty good job of helping us enforce checking for these, but we have to do it. The language and type checker can only do so much to protect us. Also, using null can be unclear. Does it mean ‘no value’ or ‘an empty value’?

What if we could behave as if null and undefined didn’t exist? FP has something that lets us (mostly) do that. It is a data type called Option. Let’s have a look at its definition from fp-ts:

An Option is a ‘container’ for an optional value of type A. If value of type A is present, the Option will be an instance of Some<A>. If no value of type A is present, the Option will be an instance of None, but there will always be an instance of Option present – which means we don’t need to deal with it being null or undefined.

Another way to think about how this works is to treat it as an array of size 0 or 1. You will always have an array, but sometimes it won’t hold any values.

All the types in fp-ts are pure data types, not classes, so fp-ts provides us with functions to help us create and manipulate those instances. For Option, you can import those functions as follows:

Idiomatically, these module functions are imported as namespace imports because a lot of the functions’ names are repeated across the various data types.

Constructing an Option

Manipulating an Option

Its probably about now you are thinking that we have just traded null checking for None checking. Well we have, yes, but also no. Let’s see an example by looking at map.

map

If opt is Some, map will take the value out of the Some, assign it to a, then run the given function f(a) and return a new Some with the result. If opt is None, map just returns None as there is no ‘value’ to apply to f. This means we don’t need to check if opt is a Some or None because map does it for us.

What happens if the function we give to map returns an Option?

How do we unnest this result?

flatten

flatten takes a nested Option and collapses it.

It only removes one level of nesting.

Passing a None or a Some of None returns None.

It turns out that having the function passed to map return an Option is very common. It would be nice if we didn’t have to keep calling map then flatten. We can use a function called chain to do just that.

chain

As with map, if opt is Some, chain will run the given function f and return its result. If opt is None, chain just returns None as there is no ‘value’ to apply to f.

We can now simplify the map then flatten code a little bit.

Testing an Option

Filter example

Getting value of an Option

Once we have an Option, at some point we want do something with the actual value it holds. This usually happens at the edges of our program. All of the functions provided by fp-ts make us handle the fact that it could be Some or None.

getOrElse

If option is Some, getOrElse returns the value inside option. If option is None, getOrElse returns the result of the passed in onNone function. Note that the return type of the onNone function has to be the same type as the one in the Option.

fold

If option is Some, fold returns the result of the passed in onSome function called with the value inside option. If option is None, fold returns the result of the passed in onNone function. Note that here the return type of the onNone and onSome functions has to be the same, but it can be a different type as the one in the Option.

toNullable, toUndefined

If option is Some these functions return value inside option. If option is None these functions return either null or undefined

Either – Handling errors

The most common way that Typescript programs deal with error conditions is to throw an error and leave it up to the caller (or something in the call hierarchy) to handle it with a try/catch block. There are a few issues with this:

  • throw isn’t a great way to propagate errors as it is control statement – ie: it changes what your program executes. This makes it harder to reason about what your program will do when an error happens.
  • You don’t have to handle a thrown error, but if you don’t, it will probably cause a bad user experience.
  • Typescript has no way of specifying “this might fail, you should deal with it”. You just have to know that the thing you are doing might fail.
  • throw doesn’t carry any type information, so there is no way to say “my function will fail with this type of error”

The ‘FP’ way of dealing with things that might fail is by using something called an Either.

An Either represents a value of one of two possible types – E and A.

Left and Right have no inherent meaning. By convention, Left is used to represent a failure or error and Right is used to represent a successful result. A good way to remember the meaning of left vs right is that “right” is a synonym for “correct”.

In fp-ts, Either is only for handling synchronous operations that might fail. If Promises are involved, Either probably isn’t the correct thing. We will see the right thing later when we visit TaskEither.

Importing Either

Constructing an Either

But didn’t we say one of the purposes of Either was to replace throw and try/catch?

This runs f(). If it succeeds, it will return a Right<A>. If f() throws, onThrow is called with the error (or string or whatever value is thrown). Its result will be returned as a Left. Because catch clauses cannot have a type annotation in Typescript, the type of e is unknown so you will need to convert it to the expected type (E).

Manipulating an Either

Remember map? It’s back.

map

We pass a function f that takes a value of type A and returns a value of type B, same as O.map. If we pass Right<A> as either, map will take the value out of the Right<A>, assign it to a, then run f(a) and return a new Right<A> with its result.

But what happens if pass a Left<E>?

Let us look closer at the type signature of map. We can see f converts a value of type A to a value of type B, there is nothing that converts values of type E. This means if we pass a Left<E>, the Left<E> is returned unmodified. But what if we want to map a Left?

mapLeft

mapLeft only operates on a Left. If you pass it a Right, the Right will be returned unmodified.

flatten

Just like with Option, E.flatten combines two layers of Either into one, but only if the outermost Either is a Right. It will only ever return a Right if both layers are Right.

Look what else is back – chain.

chain

As with map, if either is a Right, chain will run the given function f and return its result. If either is a Left, chain will return it unmodified without running f.

This behaviour is also called “short-circuiting” and is common across fp-ts functions.

Testing an Either

Getting value of an Either

Like with Option, we will at some point want to get the value out of the option and work with it directly. Again, like Option, this usually happens at the edges of our program.

getOrElse

If either is Right, getOrElse returns the value inside either. If either is Left, getOrElse returns the result of the passed in onLeft function using the value from either. Note that this function makes us handle the case where the Either represents a failure (ie: it is a Left).

fold

If either is Right, fold returns the result of the passed in onRight function called with the value inside either. If either is Left, fold returns the result of the passed in onLeft function using the value from either. Again, we must handle both the Left and Right cases.

Takeaways

Option is used in place of null/undefined. It represents the presence or absence of a value.
Either is used to represent the result of a synchronous operation that might fail. Its type signature means that we can see what the type will be in case of a ‘failure’.

Wrap up

We’ve seen two different data types that share some common functions. fp-ts provides many other data types that also provide map and chain functions such as Array, NonEmptyArray and Set. There is a reason for this which is outside the scope of this post, but if you are interested, look up ‘typeclasses’ and start with ‘Functor’.

In the next post we will look at types from fp-ts that help us deal with asynchronous code/promises.

View jobs

More from the blog

My first rotation as a Springboarder at REA.

Category: Career stories
Published on

From Teaching to Software Development – A Year On

Category: Career stories
Published on

A vision of the future is a bet we are prepared to make

Category: Tech
Published on

One year at REA: Lessons, Growth, and Gratitude

Category: Life at REA
Published on

Introducing Argonaut – Part Three.

Category: Tech
Published on

Introducing Argonaut – Part Two.

Category: Tech
Published on

View all articles