DEV Community

Derp
Derp

Posted on

Intro to fp-ts

This article serves as an introduction to fp-ts coming from someone knowledgeable in JS. I was inspired to write this up to help some team members familiarise themselves with an existing codebase using fp-ts.

Hindley-Milner type signatures

Hindely-Milner type signatures represent the shape of a function. This is important because it will be one of the first things you look at to understand what a function does. So for example:

const add1 = (num:number) => num + 1
Enter fullscreen mode Exit fullscreen mode

This will have the type signature of number -> number as it will take in a number and return a number.

Now what about functions that take multiple arguments, for example a function add that adds two numbers together? Generally, functional programmers prefer to functions to have one argument. So the type signature of add will look like number -> number -> number and the implementation will look like this:

const add = (num1: number) => (num2: number) => num1 + num2
Enter fullscreen mode Exit fullscreen mode

Breaking this down, we don't have a function that takes in two numbers and adds them, we have a function that takes in a number and returns another function that takes in an number that finally adds them both together.

In the fp-ts documentation, we read the typescript signature to tell us what a function does. So for example the trimLeft function in the string package has a signature of

export declare const trimLeft: (s: string) => string
Enter fullscreen mode Exit fullscreen mode

which tell us that it is a function that takes in a string and returns a string.

Higher kinded types

Similar to how higher order functions like map require a function to be passed in, a higher kinded type is a type that requires another type to be passed in. For example,

let list: Array<string>;
Enter fullscreen mode Exit fullscreen mode

Array is a higher kinded type that requires another type string to be passed in. If we left it out, typescript will complain at you and ask you "an array of what?". An example of this in fp-ts is the flatten function from array.

export declare const flatten: <A>(mma: A[][]) => A[]
Enter fullscreen mode Exit fullscreen mode

What this says is that the function requires another type A and it flattens arrays of arrays of A into arrays of A.

However, arrays are not the only higher kinded types around. I like to think of higher kinded types as containers that help abstract away some concept. For example, arrays abstract away the concept of iteration and Options abstract away the concept of null.

Option

Options are a higher kinded type that abstract away the concept of null. Although it requires some understanding to use and some plumbing to get it all set up, my promise to you is that if you start using Options, your code will be more reliable and readable.

Options containers for optional values.

type Option<A> = None | Some<A>
Enter fullscreen mode Exit fullscreen mode

At any one time, an option is either a None representing null or Some<A> representing some value of type A.

If you have a function that returns an Option for example head

export declare const head: <A>(as: A[]) => Option<A>
Enter fullscreen mode Exit fullscreen mode

By seeing that the function returns an Option, you know that by calling head, you may not get a value. However, by wrapping this concept up in an Option, you only need to deal with the null case when you unwrap it.

So how do you write your own function that returns an Option? If you are instantiating your own Options, you will need to look under the constructors part of the documentation. For example,

import { some, none } from "fp-ts/lib/Option";

const some1 = (s:string):Option<number> => s === 'one'? some(1) : none;
Enter fullscreen mode Exit fullscreen mode

However to extract out the value inside an Option, you will need to use one of the destructor methods. For example, the fold function in Option is a destructor.

export declare const fold: <A, B>(onNone: Lazy<B>, onSome: (a: A) => B) => (ma: Option<A>) => B
Enter fullscreen mode Exit fullscreen mode

This type signature is a little complicated so let's break it down.

  • fold: <A, B>...:This function has two type parameters A & B
  • ...(onNone:Lazy<B>,...: This take in an onNone function that returns a value of type B
  • ..., onSome: (a: A) => B)...: This also takes in an onSome function that takes in a value of type A and returns a value of type B
  • ... => (ma: Option<A>)...: This expects an Option of type A to be passed in
  • ... => B: After all arguments are passed in, this will return a value of type B.

Putting all this together, if we wanted to use our some1 function from earlier and print "success 1" if the value was "one" otherwise print "failed", it would look like this:

import { some, none, fold } from "fp-ts/lib/Option";

const some1 = (s:string):Option<number> => s === 'one'? some(1) : none;

const print = (opt:Option<number>):string => {
  const onNone = () => "failed";
  const onSome = (a:number) => `success ${a}`;
  return fold(onNone, onSome)(opt);
}

console.log(print(some1("one")));
console.log(print(some1("not one")));
Enter fullscreen mode Exit fullscreen mode

Now we know how to create an Option as well as extract out a value from an Option, however we are missing what in my opinion is the exciting part of Options which is the ability to transform them. Options are Functors which is a fancy way of saying that you can map them. In the documentation, you can see that Option has a Functor instance and a corresponding map instance operation.
What this means is that you can transform Options using regular functions. For example, if you wanted to write a function that adds one to a an Option<number> it would look like so:

import { map, Option } from "fp-ts/lib/Option";
const add1 = (num: number) => num + 1;

const add1Option = (optNum:Option<number>):Option<number> => map(add1)(optNum);
Enter fullscreen mode Exit fullscreen mode

Now we know how to create options, transform them via map functions and use destructors to extract out the value from them whilst referring to the documentation each step of the way.

Discussion (0)