loading...
Meeshkan

Functional on the frontend with fp-ts and pipe

carolstran profile image Carolyn Stransky Originally published at meeshkan.com ・7 min read

This article is based on an internal presentation done by Mike Solomon, who will be further referred to as "my boss."

As a team, we decided to integrate functional programming practices into the codebase for our web application. More specifically, we're using fp-ts, a library for typed functional programming in TypeScript.

This article explains why we chose fp-ts and walks through a practical example using the pipe function.

In this article:

Why we're going functional

Because my boss likes Haskell 🤷‍♀️

I'm joking (mostly). My boss does have an affinity for functional programming and he's more comfortable in this type of workflow. But even if the learning curve is steep for those of us who didn't know what monads are, we've realized something. Adopting functional programming practices has improved our web application.

Here are some of the reasons:

Productivity

  • Descriptive errors - When we see logs in the console, it's rarely Uncaught TypeError: Cannot Read Property 'name' of undefined or Object doesn't support property or method 'getPosts'. This helps for more efficient debugging.
  • Less code - Functional programming takes care of many patterns that would otherwise result in boilerplate code.
  • Limited options - With functional programming, you can only do things a certain number of ways.
  • Refactoring - With strong type safety, you refactor "against" the compiler. This means the red squiggles in your IDE guide the refactoring process and proposes helpful suggestions.

Correctness

  • Type safety - When you use a typed variable, you're defining a constraint on all possible values. This helps ensure that the inputs and outputs of our code work as expected.
  • Error routing - With functional programming, errors become first-class citizens and are propagated to error handlers based on rules.
  • Linear ordering - No more jumping between if this else that or getting stuck in a deep-nested JavaScript try/catch block.

Why we chose the fp-ts library

In theory, we could've switched out fp-ts for another functional programming library for TypeScript like Purify. Both libraries have similar syntax for common functional patterns like the Either class and the chain function. However, fp-ts has some additional classes that we use regularly like Reader and Semigroup.

If there were terms in that last paragraph that you didn't understand, don't worry! We'll cover those in a future post.

Working with our existing React codebase

Fortunately for us, the codebase we're working with is still fairly new. The repository was created a little over one month ago. The initial setup was done by two developers (myself included) with no functional programming experience. But, turns out, we were already applying functional programming principles to our React application.

Some examples:

But taking that next step into the functional programming world required us to restructure the way we think about and read code. To make it more tangible, the rest of this article will focus on one specific function from the fp-ts library: pipe.

Putting it into practice with pipe

The concept of piping goes well beyond the fp-ts library. According to The Linux Information Project, piping is defined as:

A form of redirection that is used to send the output of one program to another program for further processing.

Sounds intense and a bit abstract. Let's break it down.

Overall, a pipe is one big function of functions. It takes an initial value and then passes that as the argument(s) for the first internal function to use. Then it takes the result from that function and passes it to another internal function. And so on, potentially forever 🤪

Maybe it's better to explain with code.

Here's an example of piping written in vanilla JavaScript:

const examplePipe = (a, b, c) => c(b(a));

This examplePipe function takes in three parameters (a, b, and c). For examplePipe to work as expected, a should be a value that can be consumed by b. Then b should be a function that takes a as an argument. Finally, c should be another function that takes the result of b as an argument.

Let's put in some arguments:

examplePipe(1, (x) => x+1, (x) => x+5)

First, it takes an independent value: 1.

Then, 1 is passed to the next function: (x) => x+1. So because x is equal to 1, the result is 2.

Finally, this result (2) is passed to the last function: (x) => x+5. Because x is now equal to 2, the examplePipe will return 7.

And there you have it, our first pipe 🎉

This was a generic example of piping. Next, we'll go step-by-step to see how this would work in a web application. Throughout, we'll use the pipe function that's available through the fp-ts library.

Defining the initial value in a pipe

The most minimal pipe we can write is a pipe with a single object, like pipe(1). Here, the first value (1) isn't consumed by any functions in the pipe. This means that the result of pipe(1) is equal to 1.

As soon as a pipe grows to two values, it then enforces a contract - the second element of the pipe must be a function that can consume the first value. This first value can be anything: A number, a string, a class, a function, or even void.

This is common practice in functional programming. Instead of defining variables along the way, everything we need is defined at the start. "Priming the pipe" so to speak.

Let's start creating an example. We're going to define an exampleFunction that doesn't have any parameters and returns a pipe. To start, pipe contains an object with three values: projects (independent getProjects function), a users array, and a configuration object.

It should look like this:

const getProjects = () => ([]);

const exampleFunction = () => pipe(
    {
        projects: getProjects(),
        users: [5],
        configuration: {}
    }
);

This example assumes we have fp-ts installed and pipe imported.

Another nuance of pipe is the order (or lack of order) that we define our initial values. To show how this works, let's look at a real-world example.

In our web application, we often define our hooks within this first part of the pipe function. Alternatively, you could use const to define variables like so:

const useColorMode = useColorMode()
const useDisclosure = useDisclosure()

In this structure, useDisclosure will always be executed after useColorMode. This is because JavaScript code executes in order.

But with an object, there are no guarantees about the order of execution. JavaScript doesn't indicate which values in an object are created in memory first. This is true for any object, but it's especially useful in our pipe function.

Defining variables within the first object of pipe signals to anyone maintaining the code that the order of these variables is insignificant. This allows us to refactor with more confidence.

What's also nice about putting these values first is that it distinguishes what is independent in your function. So no matter what, you know that these values don't have any dependencies or rely on anything else. This can help with debugging and code readability.

First function in the pipe

The next part of the pipe is our first function. In this function, we can pass the values defined in the first object as an argument.

We do this in the following example with the valuesFromObjectAbove parameter:

const getProjects = () => ([]);

const exampleFunction = () => pipe(
    {
        projects: getProjects(),
        users: [5],
        configuration: {}
    },
    (valuesFromObjectAbove) => ({
        // Coming soon!
    })
);

Here, valuesFromObjectAbove represents projects, users, and configuration.

We can then use valuesFromObjectAbove to create new values. In this example, we're creating arrays of adminProjects and notAdminProjects using the projects value we defined in the first object:

const getProjects = () => ([]);

const exampleFunction = () => pipe(
    {
        projects: getProjects(),
        users: [5],
        configuration: {}
    },
    (valuesFromObjectAbove) => ({
        adminProjects: valuesFromObjectAbove.projects.filter(a => a.admin === true),
        notAdminProjects: valuesFromObjectAbove.projects.filter(a => a.admin === false)
    })
);

Now, we can see this grouping of independent values first, dependent ones second. Reading the code, we can deduce that adminProjects and notAdminProjects, by definition, depend on a value that was created earlier. This can help with debugging. For instance, if you insert a console.log() statement after the first object, you know that your log will only contain the independent values in the function.

Another round of functions

There are a few options available for what values are passed to our second function.

One option is to use a spread operator:

const getProjects = () => ([]);

const exampleFunction = () => pipe(
    {
        projects: getProjects(),
        users: [5],
        configuration: {}
    },
    (valuesFromObjectAbove) => ({
        ...valuesFromObjectAbove, // Look here!
        adminProjects: valuesFromObjectAbove.projects.filter(a => a.admin === true),
        notAdminProjects: valuesFromObjectAbove.projects.filter(a => a.admin === false)
    }),
    (valuesFromFunctionAbove) => ({
        ...
    })
);

By using the spread operator, we're saying that we want to pass down everything. This means that valuesFromFunctionAbove contains all of the values from the initial object (projects, users, configuration). And it also contains the values from the first function (adminProjects, notAdminProjects). Bonus: It's all type safe!

But let's say we delete the spread operator:

const getProjects = () => ([]);

const exampleFunction = () => pipe(
    {
        projects: getProjects(),
        users: [5],
        configuration: {}
    },
    (valuesFromObjectAbove) => ({
        // No spread operator
        adminProjects: valuesFromObjectAbove.projects.filter(a => a.admin === true),
        notAdminProjects: valuesFromObjectAbove.projects.filter(a => a.admin === false)
    }),
    (valuesFromFunctionAbove) => ({
        ...
    })
);

Now, the second function only has access to adminProjects and notAdminProjects.

That is the power of pipe. We always know what's ready to use 💥

If organized appropriately, pipe can contain everything that we would need to create our React component. So those ... in the last two examples? That's where we could put in our JSX.

More with fp-ts

This article only scratched the surface of what the fp-ts library can bring to a web application. On our team, there are many more functions and patterns that we use (Either, chain, isLeft, isRight, Reader). If you'd be interested in learning about these, tweet at us or leave a comment and let us know!

In the meantime, check out the fp-ts documentation.

Posted on May 27 by:

carolstran profile

Carolyn Stransky

@carolstran

Software developer and sometimes still a journalist (she/her)

Meeshkan

An automated testing workflow for your project and its dependencies. Using stateful property-based testing, we find bugs your linter could only dream of. We’re currently in private alpha and accepting applications!

Discussion

markdown guide
 

Hello Carolyn,

that is fairly didactical article, I like how you introduce and develop the subject. There is indeed a lot of functional patterns we can already put in use in our code without needing a pure functional language. It is also quite exciting to see companies that are using fp-ts in earnest. Two years ago, I wish there would have been more example code of how to put all this to good usage. They came a long way since then in terms of docs and examples.

I could not however find the reasoning behind those assertions:

  • Hooks as a functional way to manage state dependencies.
    • How are hooks functional? Is it because they are functions? But they are effectful functions (with no indication that they are so, except the prefix use). The first time they run and the second time they run with the same argument they do not do the same thing, by design. That design is also why there are the rule for hooks to follow. I confess I haven't used hooks so I maybe wrong. But a year or so, that was the conclusion I reached. That may have changed.
  • Function components instead of class components
    • that's a bit linked to the previous point. A pure function need not be a class. But if you need a class, and you replace that by a function component, that means your function component is not pure. You can't replace something impure by something pure. Then the previous point applies again.

The bottom line is that functional programming is not about the syntax, i.e. it is not just about using functions (who doesn't?). The essential pattern is expressing computations as the composition of pure functions. Hooks, in that matter, are a bit confusing because they compose (when applied in order, and within a React context) but they do not compose like pure functions do. That's better than not composing, but still, I would not call using hooks applying functional programming principles.

But that does not really matter though. The more important point is that I am going to check Meeshkan, PBT being something I have a strong interest in :-) Automatic testing for any app -> that is exactly what I call a problem worth solving.