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:
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:
// ... 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.
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.
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:
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- IOEitheris purely sync but- TaskEitheris not.- fromIOEitherallows us to convert sync- IOEitherto a matching- TaskEitherstructure.
- If you are unfamiliar with - chainmethod, it allows us to escape nested structures and still map one, in this case,- TaskEitherto another one.
- saveDatamethod has this curry-like signature to allow for the creation of independent save managers that has- pathprepended.
- I'm using - flowmethod 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.
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.
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
Thanks 👋
Additional resources
- There is a great series that covers fp-ts in much greater detail than I ever could. Give it a read! 
- Kyle Simpson has a great series on FrontendMasters 
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.
 

 
    
Top comments (0)