FP-TS Functional Programming in TypeScript

Intro to Functional Programming in TypeScript with FP-TS

Nicholas Hammond portrait

Nicholas Hammond

13 October 2021 · 5 min read

This article is part of a series about functional programming with FP-TS. It's aimed at those who may have heard good things about functional programming but don't really know where to start approaching the subject. More specifically it's for JavaScript users who may be aware that their language has something to do with functional programming but not really sure how. Let's get started!

What is FP-TS?

By some definition, one might call JavaScript a functional language due to the fact it has first class functions. However, ask a functional programmer and they'll most likely balk at such a description as functional programming is much broader in scope.

TypeScript does begin to address these discrepancies by including types. While this is an important start, it really is only a start.

fp-ts is an additional layer on top of TypeScript, which leverages existing capabilities and gives us the full power of the functional paradigm. More precisely, it is a TypeScript library which provides additional methods to capture all the concepts a functional programmer would need.

What are pipelines?

One of the primary philosophies of functional programming is that you don't mutate data. Instead the idea is that you have an input which is "piped" through a function. The important point here is no data is mutated; the source of the input is the unaffected by the function and the output is new data.

You can connect these "pipes" together into a longer pipe, much like how actual physical pipes are put together. We could write this as:

input -> f1 -> output1 -> f2 -> output2 -> f3

If you have ever seen any Elixir, F# or Elm code one might see this written with the following syntax:

input |> f1 |> f2 |> f3

If you have used Unix pipes then it's also the same thing, except with lines of text piped from stdin to stdout / stderr.

cat file.txt | wc -l

Why are pipelines useful?

So why is this useful? That is a good question. After all, even spaghetti code can technically perform any task we might want. The answer lies in the fact that there is more to writing code than simply getting it to compile. For some the aesthetics of the good code is reward enough, but for others who are more practically minded and aren't interested in becoming one with the code, there are other incentives.

First of all if we build our programs out of functions without side-effects, then reasoning about your code becomes much simpler. We can be confident that we aren't going to get nasty side effects from unintentionally mutating data which other code may be using.

Secondly, it allows us to read and write our code as a series of self contained functions which reads more easily; no parsing of complicated 'for' loops and 'if' statements is required to see what is happening at the top level of the pipeline. If you need to see the detail, then you can inspect each function in the pipeline in isolation.

If we wanted to try and achieve the same effect in TypeScript where we pass the value of one function to another, the syntax forces us to write something awkward like:

output = f3(f2(f1(input)))

The Pipe operator in FP-TS

fp-ts gives us the pipe operator to create a pipeline of function:

import { pipe } from 'fp-ts/lib/function'

const f1 = (x) => x + 1
const f2 = (x) => x + 2
const f3 = (x) => x + 3

pipe(0, f1, f2, f3)  // returns 6

When we use this in a real program this let's us reason about high level program flow more easily:

pipe(query, fetchFromDatastore, transformResult)

You can find some more examples in the FP-TS pipe documentation.

The Flow operator in FP-TS

fp-ts gives us another very similar tool called the flow operator. It's very similar to pipe except that all the arguments are functions and the output it returns is itself a function which one then passes in the initial input.

More concretely:

import { flow } from 'fp-ts/lib/function'

const splitBy = (char: string) => (s: string) => s.split(char)
const count = (xs: unknown[]) => xs.length
const splitBySpace = splitBy(' ')
const countWords = flow(
  splitBySpace,
  count
)

countWords('a b c')  // returns 3

The advantage of flow is that if you want to pass the result of a series of piped functions into another function, you don't need write an anonymous function.

Instead of:

someOtherFunction = (num: number, func: (number) => number)
someOtherFunction(1, (num) => pipe(num, f1, f2, f3))

We can do:

someOtherFunction = (num: number, func: (number) => number)
someOtherFunction(1, flow(f1, f2, f3))

You can find some more examples in the FP-TS flow documentation.

Currying with FP-TS

What if we want to add functions with multiple arguments to a pipeline? This won't work because pipe is expecting a single argument function. There is a technique which we can take advantage of to help us.

Currying is the conversion of a function with multiple arguments into a series of single argument functions. For example, a 3 argument function f(x, y, z) becomes ((g(x))(y))(z) or g(x)(y)(z). Each new single argument function returns a new function which again takes the next argument until last function just returns the result.

So instead of:

declare const f: (x: A, y: B, z: C) => D

We can define a new curried version:

declare const curriedF = (x: A) => (y: B) => (z: C) => { return f(x, y, z) }

So that:

const first: (y: B) => (z: C) => D = curriedF(a)
const second: (z: C) => D          = curriedF(a)(b)
const returnValue: D               = curriedF(a)(b)(c)

This resembles piping but instead of passing the output of a function to the next function, we can think of it as sending a function to a value. In fact if we had a function which takes values and returns a function which takes a curried function and returns the next curried function in the sequence, we could use piping to get this behaviour. As it so happens, fp-ts provides just such a function, ap which is short for "apply".

declare const ap: <A>(arg: A) => <B>(f: (arg:A) => B) => B

ap :: a -> (a -> b) -> b 

This means we can combine this with piping to get the following:

const returnValue = pipe(curriedF, ap(a), ap(b), ap(c))

What happens here is that we pass the curried version of our function f() along the pipeline passing in values one by one. This might seem a little convoluted In practice we would probably use sequences to do this kind of thing rather than ap, however that's a topic for another post.

Conclusion

We've really only just scratched the surface of what fp-ts is about, but hopefully this article has provided some insight into functional programming techniques and the clarity it can bring to your code. In particular the pipe and flow operator gives us a way to express our program in a more linear way.

This is just the beginning however and we shall dive deeper into fp-ts in later articles. So stay tuned!

References

Nicholas Hammond portrait

WRITTEN BY

Nicholas Hammond

Hi, I’m Nicholas! I'm an analytically and creatively minded software developer who is always learning new technology. I've worked with TypeScript, React, GraphQL, Elixir, Ruby on Rails, HTML, and CSS. I have a deep love of pure mathematics and digital animation.

More articles

Customers providing feedback on a product

Strategy & Advisory

Do your tech choices empower a test and learn cycle?

Ben Melbourne profile picture

Ben Melbourne

1 February 2024 – 2 min read

Padlock constructed from triangles as imagined by Midjourney

Ash

Customising Ash Authentication with Phoenix LiveView

Will Tan

Will Tan

19 March 2023 – 4 min read

Want to read more?

The latest news, articles, and resources, sent to your inbox occasionally.