My brief rendez-vous with fp-ts
Tags: webdev, functional
Published: 2021-03-12 09:32:08

---
title: My brief rendez-vous with fp-ts
published: true
description: The best reason not to use functional programming in your stack
tags: webdev, functional
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mxknp9deyvc7cao494y4.png
---

> Disclaimer: I am a _huge_ fan of the `fp-ts` project and think it is one of the greatest, if not the greatest, `typescript` libraries on the planet. This article is in no way meant to disparage `fp-ts`, but rather to describe some (what I believe to be) legitimate reasons _not_ to use it and provide context around my thinking.

Almost every article I've written on dev.to over the past year has touched functional programming in some form or another. They've ranged over subjects like:

- [Modeling asynchronous transactions with types](https://dev.to/mikesol/modeling-asynchronous-transactions-with-types-part-4-1dk3)
- [Composing readers](https://dev.to/mikesol/composing-readers-cjp)
- [Piloting Puppeteer with PureScript](https://dev.to/mikesol/piloting-puppeteer-with-purescript-part-1-dpf)
- [Freeing Free Monads with Free ADTs](https://dev.to/mikesol/freeing-free-monads-with-free-adts-5gpi)
- [Dead Simple GraphQL with Typeclasses and functional dependenceis](https://dev.to/mikesol/dead-simple-graphql-with-typeclasses-and-functional-dependencies-2jdl)
- [...](https://dev.to/mikesol)

In this article, I'd like to discuss a great reason _not_ to use functional programming. It comes from a short-lived period (~30m) where `fp-ts` was in, then out, of our stack.

In the first part of the article, I'll show how I used `fp-ts` to do pattern matching on an internal data format. In the second, I'll discuss why this didn't work and what the alternative solution was. And in the third part, I'll get a bit meta and discuss the why and when of functional programming.

# Hello fp-ts

At [Meeshkan](https://meeshkan.com), we use AI to generate Selenium and Puppeteer tests of webapps based on real user behavior.  One of our clients recently asked us to export these scripts so that they could be run in their private cloud.

We decided to embed the exporter in our webapp, as the app has access to all the data needed to generate a script and it's way faster for us to ship a client-side feature than a backend feature.

Because each command in the script could be one of several actions, my brain automatically went to [variants](https://en.wikipedia.org/wiki/Algebraic_data_type).  In languages like Haskell, PureScript, OCaml and Elm, variants work like this:

```haskell
data Comamnd =
  NavigateToPage {url :: String }
  | ClickOnElement {xpath :: String }
  | EnterInput {xpath :: String, value ::String }
```

In TypeScript, you can sort of do this.

```typescript
type Command = {url :: String } | {xpath :: String } | {xpath :: String, value ::String }
```

This is called an _untagged union_, and the issue with it is that it is a one-way street. While you can get data into the untagged union, getting it out is a pain because there is no clean way to discriminate between values. The closest you can get is the [`is`](https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards) operator, which kinda works but requires you specifying [what the definition of `is` is](https://www.youtube.com/watch?v=BS-cip2brsg), which _is_ (😉) prone to error. It also does not work for data structures that have identical fields or data-types that are subsets of another data type.

`fp-ts`, on the other hand, allows you to operate on tagged unions. The tagged union I wrote was called `Command`, and I wrote functions to move objects into the tagged union with these signatures.

```typescript
const open: (o: Open) => ScriptCommand;
const setViewportSize: (o: SetViewportSize) => ScriptCommand;
const click: (o: Click) => ScriptCommand;
const type: (o: Type) => ScriptCommand;
const dragndrop: (o: Dragndrop) => ScriptCommand;
```

And then, to get out of the tagged union, you need a generic function from all possible variants to the output type. It's type is (was...can types die?) this:

```typescript
const transformCommand = <T>(c: {
  open: (o: Open) => T;
  setViewportSize: (s: SetViewportSize) => T;
  click: (c: Click) => T;
  type: (t: Type) => T;
  dragndrop: (d: Dragndrop) => T;
}) => (command: ScriptCommand): T
```

If you give it a dictionary of converters to type `T` and a command, it will output something of type `T`.

The implementation using `fp-ts`-'s `fold` is a one-liner:

```typescript
const transformCommand = <T>(c: {
  open: (o: Open) => T;
  setViewportSize: (s: SetViewportSize) => T;
  click: (c: Click) => T;
  type: (t: Type) => T;
  dragndrop: (d: Dragndrop) => T;
}) => (command: ScriptCommand): T =>
  fold(c.open, fold(c.setViewportSize, fold(c.click, fold(c.type, c.dragndrop)))
)(command);
```

Then when I wanted to convert our command to a string of Puppeteer commands, I gave it a dictionary of transformers to Puppeteer and the `fp-ts` took care of the rest thanks to `fold`.

I wrote the code this way because it was comfortable for me, but at the same time, I knew that several of our team members had little experience with `fp-ts` and/or had negative experiences of not understanding code written using `fp-ts`.  It's not hard to see why: in the example above, if you don't know what `fold` is doing (and in general if you don't know what folding is or pattern matching is from functional languages), it is _really hard_ to grok how `transformCommand` works.

# Goodbye fp-ts

After a talk with our lead designer / lead web developer / product manager / COO Makenna, she expressed that she was uncomfortable with `fp-ts` and that the same outcome could be accomplished by just converting the data from 8base to a string without going through an intermediary format. She was absolutely right: in terms of our business logic, the intermediary format has no benefit as the data would always be coming from 8base.  The code became:

```typescript
const eightBaseToX = (formatter: {
  open: (o: Open) => string;
  setViewportSize: (o: SetViewportSize) => string;
  click: (o: Click) => string;
  type: (o: Type) => string;
  dragndrop: (o: Dragndrop) => string;
}) => (
  script: SeleniumScript,
  options: ScriptToPptrOptions
): string | undefined;
```

You have to squint to see the difference. It is _exactly_ the same as before except it is operating on the data from 8base, which is a bunch of nullable fields organized more or less as a sequence of executable selenium commands, to a Puppeteer script. The _only_ difference is that it cuts out the intermediary format, aka our tagged union `ScriptCommand`. But as we'd only ever be getting the data from 8base, there's no need for an intermediary format.

The refactor took 10 minutes and is ~50 less lines of code, doesn't introduce a new dependency, and still uses a functional style, all in a way that my team can understand and maintain.  Why _wouldn't_ anyone want this?

At the end of the day, `fp-ts` only would have made sense if Makenna and others were comfortable with it enough to share its maintenance burden. Otherwise, by all objective standards, I had written _worse_ code with `fp-ts`: it was longer and had an extra dependency.  And this is not to say that Makenna is not comfortable with functional programming - she uses a purely functional language (`jsx`), hates classes in react, lauds hooks, and has to put up with me talking about functional programming five times a day. So really, what we're talking about here is someone who is excellent at functional programming (Makenna) asking someone who is obsessive about it (me) to tone it down a notch.

What that got me thinking about, then, is when _does_ one use functional programming?  What made me the sort of developer that instinctively reaches for solutions like PureScript and `fp-ts`?  That introspection led me to create four-ish paths to functional programming. I'll present them below, and I hope these distinctions are useful to you, whether you're a seasoned veteran or just starting out with FP.

# When to use functional programming

There are four reasons I've seen people come to functional programming. I think the first two generally don't work out whereas the last two do. That doesn't mean that the first two are invalid, but rather that they do not generally lead to a sustainable outcome. They are:

1. Appetite to learn and apply new things ❌
1. Need to solve a problem ❌
1. Need to solve many recurring & interrelated subtle problems that only emerge over years of practice ✅
1. Trusting someone from category #3, ie because they were your teacher or mentor ✅

## 1. Appetite to learn and apply new things ❌

This is the most dangerous category and the one I admittedly fall into. Someone with an appetite to learn new things often winds up coding a solution to a problem that doesn't exist. This eats up valuable business time and leads to situations where people convince themselves a problem actually _does_ exist, namely that their solution is not implemented, and then solve it with an implementation using their solution.

The other danger with this approach is that it is usually based on an emotion of excitement or intrigue. When that excitement or intrigue fades, what will happen? Who on the team will maintain the code?

While I think an _ability_ to learn and apply new things is a valuable skill, an _appetite_ for new things can be destructive, and it is IMO never a good reason to adopt functional programming (or any) techniques in your stack.

## 2. Need to solve a problem ❌

Reaching for functional programming when you need to solve a problem is almost never necessary unless it is already your go-to method. Meaning that, as we saw in my `fp-ts` example, for every solution that can be achieved with a functional pattern, the same solution can be achieved with a slightly-less-functional-but-still-functional-and-now-100%-more-maintainable-by-your-team pattern. So solving a problem with purely functional techniques only makes sense in a context where these techniques are go-to methods for a group of people working together or a one-person project.

Another important thing to note here is the global imbalance in functional vs imperative programming. Chances are that, unless your company already has a reputation as a purely-functional house, your team will hire coders that are mostly comfortable with imperative code. I know that for us at Meeshkan, hiring great talent from the functional programming community was a months-long process, whereas hiring for other roles garnered many more applications. A free monad doesn't mean squat if you run out of time/money because you couldn't hire a colleague that understands what a free monad is.

So functional programming is never _the_ solution to a problem. It can be a solution because "that's the way _I_ solve problems", but there is no coding problem that can be uniquely solved with with functional programming. Take any metric of business success: sustainability, profit, advocating human rights and social justice, whatever, and I _guarantee_ you that not a single successful category will have a majority of companies that felt functional programming was a key to achieving that successful outcome. Unless, of course, the metric is "uses functional programming."

That doesn't mean that functional programming cannot be crucial for business success, but rather that _if_ it is (which is yet TBD), it will be a slow and gradual process, much like the adoption of modern computing was a slow and gradual process towards more successful business outcomes (and, let's be honest, there is a strong argument that computing has left the world worse off than it was 100 years ago due to the increased pollution, poverty, sickness and war that is orchestrated by computers). In fact, many companies that originally used computers in the 1930s and 1940s _stopped_ using them for the exact same reason that I typed `yarn remove fp-ts`: they got in the way more than they helped. Jared Diamond makes a similar argument about agriculture in [Guns, Germs and Steel](https://en.wikipedia.org/wiki/Guns,_Germs,_and_Steel). Many early agrarian societies went back to hunter-gatherer or hybrid societies before hitching ourselves to the plough for good and, similar to computers, agriculture has not necessarily been the greatest gift to our species (think nutrition, women's rights, overpopulation, etc.).

Ultimately, whether functional techniques become commonplace or not will have to do with our capacity, as a community, for collective abstraction. The same is true of computers: computers _only_ work on a species-level because of our capacity for collective abstraction. When I'm waiting for the bus and looking at the number of minutes until it arrives on a digital display, whoever wrote the code that orchestrates the clock is relying on my believing that the clock, even though it has no cogs, mechanics or physical connection to a bus, accurately conveys information about a bus's arrival time. This is not _at all_ a given - it has taken years for us, as a species, to take this shared understand more or less for granted.  If I were born 150 years ago and time traveled to today's day-and-age after ~40 years of existence, there is no guarantee I would _ever_ learn how to read, let alone how to trust, a digital display of bus times. I am only able to do that in 2021 because I live in a social context where that is a collective practice. I can observe people watching digital displays of bus times and acting on them, and I assimilate this knowledge into my own actions. The gradual adoption of functional techniques, if it happens, will happen because thousands of people work together to establish a similar context over many years.

## 3. Need to solve many recurring & interrelated subtle problems that only emerge over years of practice ✅

Although I'm one of the older people on my team, I was not around when the first hammer was invented, but my guess is that when it was, it was invented to solve many recurring & interrelated subtle problems that only emerge over years of practice. That doesn't mean that the invention itself was gradual: it could have been instantaneous and even accidental. But its acknowledgement as an invention and gradual adoption in a variety of contexts could have only happened if it solved several interrelated problems (hitting a nail, hitting a flint, hitting a tree, hitting a bison, hitting a shell...).

My path to functional programming had many false starts because I fell into categories #1 and #2, but when I _actually_ started using it for real, it was for this reason. For example, our backend recently exploded because one of our vendors was `400`-ing and `500`-ing with timeouts & sundry, and we needed to come up with a queuing system for requests that could flip between parallel and sequential execution based on this vendor's status to bring our stack back online. We wound up being able to ship something in a matter of hours. The solution wound up using the [tagless final](https://serokell.io/blog/tagless-final) technique on a `StateT` monad over a functor that controlled parallelism (ie `Fiber` in PureScript), allowing me to alternate between different executors on the fly depending on the number of `400`-s and `500`-s we were receiving. I would have _never_ been able to write something this fast that is type-safe and tested with QuickCheck ~15 years ago, and I would _never_ expect someone getting started to understand what a monad is (let alone tagless final), but now it feels natural. Because I work in a context with other folks for whom this feels natural, I can write this sort of code.

Functional programming wound up emerging as a way to act quickly, decisively, and in a future-proof manner in this many other situations. I think a lot of folks that write PureScript and Haskell feel the same way. Similarly, people I know that have been writing code for ~20 years that don't use functional programming _also_ don't routinely run into situations where it would have been particularly helpful (otherwise they'd be pretty miserable!). In my case, though, FP has been a reliable pattern time-and-time again, and I'm fortunate enough to work in a place where I can do this, in large part because I started my own company precisely so I could work in a place where I can do this.

## 4. Trusting someone from category #3, ie because they were your teacher or mentor ✅

Most folks that learn programming in 2021 learn an untyped, imperative form of programming first.  However, there is a small but growing cohort of learners that learn from a functional programmer. Teaching someone functional techniques before imperative techniques is playing with fire because it gives them a skill set that is misaligned with the job market. But if you can pull it off, I find that the person is usually better equipped to build great things with code.

One thing that is important to note is that learning imperative programming is not at all a precursor to learning the concepts behind functional programming. Combinators, the lambda calculus, first-order predicate logic and category theory all existed before imperative programming became mainstream.

Another observation I've made is that, while I've seen many people adopt functional programming techniques that stay with them for life, I've never seen someone that works in a functional style for ~15 years, transitions to imperative programming and never goes back. Those _could_ exist, but the grain is almost always the other direction. I'm not sure why this is, but it is an interesting phenomena worth mentioning.

# Conclusion

So the tl;dr is that `fp-ts` is not in Meeshkan's webapp anymore (sorry @gcanti! I still <3 the project though...). But the more important thing I wanted to write about while it was still top of mind is _why_ I took it out. Ultimately, we may put it back in, and we may even transition to using PureScript on the front end, but that would _only_ ever be because there is a team-wide _entente_ about the abstractions that underlie functional programming. Or we could go back to vanilla JavaScript. I'm completely open to the idea that, in 2-3 years, I could realize that ECMAScript 1 _was_ perfection, we sped past it, and we really need to go back.

I say all this without really knowing who the intended audience is, but I could guess that there will be two sorts of readers:

1. _Someone who doesn't use functional programming regularly and ranges from militantly against it to curious but hasn't taken the plunge yet_. If that's you, my advice would be to find a mentor (I'm always happy to mentor folks!) and let it sink in over _n_ days/weeks/months/years (it took me 2-3 years to really grok fp).
1. _Someone who works regularly in functional languages_. If that's you, and if you've survived in the industry, it's likely because you are not a fundamentalist and agree with many of these ideas. I would encourage you to share your knowledge, write articles and mentor others.

And as a parting shot, no matter what category you're in, at Meeshkan we're [always looking for talented engineers that are a great culture fit](https://meeshkan.com/about-us/jobs/)! This includes, of course, those of you who like using `fp-ts` :-D