Practical Guide to Fp-ts P1: Pipe and Flow
Ryan Lee
Posted on June 16, 2020
Introduction
This post is an introduction to fp-ts, a functional programming library for Typescript. Why should you be learning fp-ts? The first reason is better type safety. Fp‑ts allows you to make assertions about your data structures without writing user-defined type guards or using the as
operator. The second reason is expressiveness and readability. Fp-ts gives you the tools necessary to elegantly model a sequence of operations that can fail. All in all, you should add fp-ts to your repertoire of tools because it will help you write better Typescript programs.
Contrary to popular opinion, you don’t need to understand complex mathematics to learn functional programming. In truth, you just need to get a feel for how each operator works. Once you get a handle on the basic operators, you can go back and review the mathematics. With this in mind, this post and the ones that follow, I will introduce functional programming from a practical perspective, avoiding mathematical jargon unless necessary.
The Pipe Operator
The basic building block of fp-ts is the Pipe operator. Intuitively, you can use the operator to chain a sequence of functions from left-to-right. The type definition of pipe
takes an arbitrary number of arguments. The first argument can be any arbitrary value and subsequent arguments must be functions of arity one. The return type of a preceding function in the pipeline must match the input type of the subsequent function.
Let's look at how to pipe simple addition and multiplication functions.
import { pipe } from 'fp-ts/lib/function'
function add1(num: number): number {
return num + 1
}
function multiply2(num: number): number {
return num * 2
}
pipe(1, add1, multiply2) // 4
The result of this operation is 4
. How did we arrive at this result? Let’s look at the steps.
- We start with the value of
1
. -
1
is piped into the first argument ofadd1
andadd1
is evaluated to2
by adding1
. - The return value of
add1
,2
is piped into the first argumentmultiply2
and is evaluated to4
by multiplying by2
.
Currently our pipeline inputs a number and outputs a new number. Is it possible to transform the input type to another type, like a string
? The answer is yes. Let’s add a toString
function at the end of the pipeline.
function toString(num: number): string {
return `${num}`
}
pipe(1, add1, multiply2, toString) // '4'
Now our pipeline evaluates to ’4’
. What happens if we were to put toString
between add1
and multiply2
? We get a compile error because the output type of toString
, string
does not match the input type of multiply2
, number
.
pipe(1, add1, toString, multiply2)
Argument of type '(num: number) => string' is not assignable to parameter of type '(b: number) => number'.
Type 'string' is not assignable to type 'number'.ts (2345)
In short, you can use the pipe
operator to transform any value using a sequence of functions. The flow of control can be modelled as follows. 1
A -> (A->B) -> (B->C) -> (C->D)
The Flow Operator
The flow
operator is almost analogous to the pipe
operator. The difference being the first argument must be a function, rather than any arbitrary value, say a number. The first function is also allowed to have an arity of more than one.
For example, we could wrap our three functions inside of the flow
operator.
import { flow } from 'fp-ts/lib/function'
pipe(1, flow(add1, multiply2, toString))
flow(add1, multiply2, toString)(1) // this is equivalent
In comparison, to the pipe
operator, this is what the "flow" of control looks like for the flow
operator. 2
(A->B) -> (B->C) -> (C->D) -> (D->E)
What is a good use case for the flow
operator? When should you use it over the pipe
operator? A general rule of thumb is when you want to avoid using an anonymous function. In Typescript, a good example of an anonymous function are callbacks.
Lets declare a function, concat
with arity of 2
. The first argument will be a number. The second argument is a callback that takes a number as its first argument and transforms it into a string. The function returns the first value and the transformed second value as a two dimensional tuple.
function concat(a: number, transformer: (a: number) => string): [number, string] {
return [a, transformer(a)]
}
Using our previous repertoire of functions, we can compose a callback for this function. Here is an example using the pipe
operator.
concat(1, (n) => pipe(n, add1, multiply2, toString)) // [1, '4']
What’s the problem with this? The problem is we have to declare n
as part of an anonymous function to use it with the pipe
operator. You should avoid this because it puts you at risk of shadowing a variable in the outer scope. It is also more verbose.
The solution is to use the flow
operator and remove the anonymous function. This works because the return value of flow
is a function itself. The signature of this function is number -> string
which is exactly the same as the callback signature.
concat(1, flow(add1, multiply2, toString)) // [1, '4']
Posted on June 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.