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. -
1is piped into the first argument ofadd1andadd1is evaluated to2by adding1. - The return value of
add1,2is piped into the first argumentmultiply2and is evaluated to4by 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']
Top comments (0)