Safe Node.js file operations with fp-ts
Tags: typescript, javascript
Published: 2019-08-18 14:45:30

---
title: Safe Node.js file operations with fp-ts
published: true
description: Using functional structures to implement composable Node.js file operations.
tags: typescript, javascript
---
As I'm trying to get myself familiar with functional programming, I thought, I might as well put my current skills to the test and write some code. 

After juggling with thoughts, I decided to write functional, safe, file-related (read, write) wrappers for Node.js native `fs.readFile` and `fs.writeFile` methods.

> This article assumes basic knowledge of TypeScript and core concepts of functional programming like composition and partial application and the notion of Functor and Monad.

# First things first

> I'm going to, in this article, skip over some of the mathematical stuff that goes along with functional programming. While omitted here, it **should NOT be***, in general, ignored.


To get started we have to familiarize ourselves with `IO`, `Task` and `Either` functional structures

## Either

Either is a structure that has two subtypes:

- left
- right

These two subtypes carry a notion of failure (`left`) and success (`right`).

It's mostly used for making computations and transformations safe.
Let's say I want to implement `safeParseInt`. `Either` is an ideal candidate to do that.

Check this out:

```ts
import { Either, left, map, right } from 'fp-ts/lib/Either';
import { increment } from 'fp-ts/lib/function';
import { compose } from 'ramda';

function safeParse(radix: number) {
  return function(strToParse: string): Either<string, number> {
    const n = parseInt(strToParse, radix);
    return isNaN(n) ? left('Parse failed') : right(n);
  };
}

const safeBaseTenParser = safeParse(10);

// You could also use fp-ts flow method
// flow works just like pipe, so you would have to switch the order of computations.
const res = compose(
  map(increment),
  safeBaseTenParser
)('123');

console.log(res);

// { _tag: 'Right', right: 124 }
// To get the actual value you probably should use the fold method.

```
Since *`Either` is right-biased*, all our transformations (`increment` in this case) will only be applied on the actual, correct, *right* value.

As soon as we introduce *left* value, all transformations, that proceed that value, will be ignored:

```ts
// ... previous code ... //

const res = compose(
  map(val => {
    console.log('Im here');
    return val;
  }),
  safeBaseTenParser
)('js-is-awesome');

console.log(res) // { _tag: 'Left', left: 'Parse failed' }

```

`console.log` in `map` never fires. That transformation is ignored since we received *left value* from `safeBaseTenParser`. How awesome is that?


To implement the aforementioned file operations we are not going to use `Either` directly, but the notion of *left* and *right* value will be present.


## IO_(*Either*)

IO is a *computation builder* for synchronous computations. That is computations, that can cause side-effects in our program.


By using `IOEither` we are communicating that these computations can fail, and so we have to deal with *right* and *left* values.


We are going to use `IOEither` for parsing / stringifying values.

```ts
import { toError } from 'fp-ts/lib/Either'
import { IOEither, tryCatch as IOTryCatch } from 'fp-ts/lib/IOEither';

const stringifyData = (data: Todo[]) =>
  IOTryCatch(() => JSON.stringify(data), toError);

const parseStringifiedData = (data: string): IOEither<Error, Todo[]> =>
  IOTryCatch(() => JSON.parse(data), toError);
``` 

`IOTryCatch` works like a `try{}catch{}` block, but returns `IOEither` so we can compose those operations.

We are also using `toError` to forward `JSON.parse/stringify` error to *left value*.


## Task_(*Either*)

Task is the async version of `IO`. 
Since we want to reap the benefits of non-blocking async operations we need this structure to wrap the `fs.readFile` and `fs.writeFile` methods.


```ts
import { promisify } from 'util';
import fs from 'fs';
import { tryCatch as TaskEitherTryCatch } from 'fp-ts/lib/TaskEither';
import { toError } from 'fp-ts/lib/Either';

const readFromFile = promisify(fs.readFile);
const writeToFile = promisify(fs.writeFile);

export const getFileContents = (path: string) =>
  TaskEitherTryCatch(() => readFromFile(path, 'utf-8'), toError);

export const writeContentsToFile = (path: string) => (contents: string) =>
  TaskEitherTryCatch(() => writeToFile(path, contents), toError);

```

Again, we are using `tryCatch` variant here, which enables us to not worry about implementing our own `try{}catch{}` blocks.

I'm also creating `writeContentsToFile` as *higher-order function*, to make it more reusable and work nicely with composition.


## Implementation

These were the main building blocks. Let's put all the pieces together now:

```ts

import { flow } from 'fp-ts/lib/function';
import {
  chain as TaskEitherChain,
  fromIOEither,
  map as TaskEitherMap
} from 'fp-ts/lib/TaskEither';

const FILE_NAME = 'data.json';
const FILE_PATH = path.join(__dirname, `./${FILE_NAME}`);

export const getFileData = flow(
  getFileContents,
  TaskEitherChain((rawString: string) =>
    fromIOEither(parseStringifiedData(rawString))
  )
);

export const saveData = (path: string) => (data: Todo) =>
  flow(
    getFileData,
    TaskEitherMap(append(data)),
    TaskEitherChain(todos => fromIOEither(stringifyData(todos))),
    TaskEitherChain(writeContentsToFile(FILE_PATH))
  )(path);

```

A few things to note here:

- Sometimes we have to use `fromIOEither`. This is because `IOEither` is purely sync but `TaskEither` is not. `fromIOEither` allows us to convert sync `IOEither` to a matching `TaskEither` structure.

- If you are unfamiliar with `chain` method, it allows us to escape nested structures and still *map* one, in this case, `TaskEither` to another one. 

- `saveData` method has this *curry-like* signature to allow for the creation of independent *save managers* that has `path` prepended.

- I'm using `flow` method here. It works just like `pipe` (left to right).

## Usage

Saving data is pretty straight forward. We have to supply `path` and then a `Todo`.

```ts
saveData(FILE_PATH)({
  id: uuid(),
  isDone: false,
  content: 'content'
// getting the actual results using fold
})().then(either => fold(console.log, console.log)(either));
```

Getting data is very similar to saving it.

```ts
getFileData(FILE_PATH)().then(either => fold(console.log, console.log)(either));
```

`saveData` and `getFileData` represent computations that may be unsafe, because of the side-effects. By invoking them we are pulling the pin of grenade hoping for the best.

If any damage is done though, we are pretty sure to where to look for culprits because we *contained impurity* within these small, composable functions.

## Summary

So there you have it.

The world of functional programming is very vast and while I'm only a beginner in this area, I've already been able to introduce a little bit of functional magic into my codebase.

I hope some of you find this article useful.

You can follow me on twitter: [@wm_matuszewski](https://twitter.com/wm_matuszewski)

Thanks 👋

## Additional resources

- There is a [great series](https://dev.to/gcanti/getting-started-with-fp-ts-setoid-39f3) that covers fp-ts in much greater detail than I ever could. Give it a read!

- [Kyle Simpson](https://twitter.com/getify) has a [great series](https://frontendmasters.com/courses/functional-javascript-v3/) on FrontendMasters

- [This git repo](https://github.com/MostlyAdequate/mostly-adequate-guide)

## Footnotes

*One might argue that knowing how functional programming relates to math is useless. I had the same view, but after learning just enough theorems and mathematical rules that govern these structures, I found it much easier to learn new concepts, because they all are connected by mathematics.