Introduction

fp-ts is a library that enables to use functional programming in typescript.

There is an ecosystem of libraries that revolves around it. Once you can use fp-ts you can use those as well.

fp-ts implements notions and concepts from functional programming. The two pillars that are essential to understand how to use it are the following:

  • Referential transparency

  • Composition (as a design pattern)

Let's talk more in depth on what those two pillars are

Referential Transparency

This is a very simple but powerful concept. It states as follows: Everything is an Expression. When something is an Expression you can safely replace it with its corresponding value without changing the Program's behavior.

This concept is very powerful when refactoring.

To achieve this effect some rules have to be enforced.

Functions need to be pure

What does "to be pure" mean? A function is pure when it doesn't have side effects, takes in input all parameters that are needed and returns a value. Let's analyze what this means.

Doesn't have side effects

A side effect is a change outside the function, the most common way is to update an external variable. Another one is a function that may throw an error.

Think whether replaceing that function with its value will update the external variable or throw the exception

Let's analyze the case of the external variable

let externalVariable = 0;

function sum(a: number): void {
  externalVariable = externalVariable + a;
}

sum(42);

console.log(externalVariable); // 42

now we perform the substitution and see what happens

let externalVariable = 0;

function sum(a: number): void {
  externalVariable = externalVariable + a;
}

42; // here we replace it's result

console.log(externalVariable); // 0

as you can see the result is changed, what can we do to rectify this?

function sum(a: number, b: number): number {
  return a + b;
}

const result = sum(21, 21);

console.log(result); // 42

now if we replace the function sum with its result the program doesn't change.

function sum(a: number, b: number): number {
  return a + b;
}

const result = 42;

console.log(result); // 42, same as before

if the function throws an error the concept is similar

function divide(a: number, b: number): number {
  if (b == 0) throw "cannot divide by zero";
  return a / b;
}

try {
  const result = divide(150, 0);
  console.log(result);
} catch (e) {
  console.log(e); // 'cannot divide by zero'
}

Now the program behavior changes when we replace the function with its result

function divide(a: number, b: number): number {
  if (b == 0) throw "cannot divide by zero";
  return a / b;
}

try {
  const result = Infinity;
  console.log(result); // Infinity
} catch (e) {
  console.log(e);
}

All parameters are in input

This is in a similar vein as before, a function needs to be able to work without external values. For example let's assume that we have a global variable and a function that depends on said global variable

const universe = {
  name: "functional programming"
};

function getFirstLetter(): string {
  return universe.name.substring(0, 1);
}

console.log(getFirstLetter()); // 'f'

Now if we move the function in a new file it doesn't work anymore

function getFirstLetter(): string {
  return universe.name.substring(0, 1);
}

console.log(getFirstLetter()); // cannot compile

to make this function pure we need to make the dependency explicit

function getFirstLetter(str: string): string {
  return universe.name.substring(0, 1);
}

console.log(getFirstLetter("functional programming")); // 'f'

This aids readability and testability of the function

Returns a value

This particular point is not so obvious, usually if something has a void return it usually means that some side effect is performed. It's in the same vein as before, portability and testability.

To perform side effect take a look at IO

Composition

Composition is a pattern that enables the construction of bigger and complex entities by combining smaller and easier to understand units that do something very specific.

Combinators

This is taken straight from Haskell

A style of organizing libraries centered around the idea of combining things. Usually there is some type T, some "primitive" values of type T, and some "combinators" which can combine values of type T in various ways to build up more complex values of type T

The general form of a combinator is:

combinator: Thing => Thing

The purpose of a combinator is to create new "things" from "things" that are already defined.

The result can be passed as an input and we get an explosion of combinatorial possibilities, this is the power of this pattern.

If we mix different combinators together the resulting combinatorial explosion is even greater.

So the general design that you will find in a functional module is achieved by using the following points:

  • a group of simple "primitives"

  • a group of combinators to combine said primitives in more complex structures

Last updated