loading...

Interoperability with non functional code using fp-ts

gcanti profile image Giulio Canti Updated on ・2 min read

Sometimes you are forced to interoperate with code not written in a functional style, let's see how to deal with it.

Sentinels

Use case: an API that may fail and returns a special value of the codomain.

Example: Array.prototype.findIndex

Solution: Option

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

function findIndex<A>(as: Array<A>, predicate: (a: A) => boolean): Option<number> {
  const index = as.findIndex(predicate)
  return index === -1 ? none : some(index)
}

undefined and null

Use case: an API that may fail and returns undefined (or null).

Example: Array.prototype.find

Solution: Option, fromNullable

import { Option, fromNullable } from 'fp-ts/lib/Option'

function find<A>(as: Array<A>, predicate: (a: A) => boolean): Option<A> {
  return fromNullable(as.find(predicate))
}

Exceptions

Use case: an API that may throw.

Example: JSON.parse

Solution: Either, tryCatch

import { Either, tryCatch } from 'fp-ts/lib/Either'

function parse(s: string): Either<Error, unknown> {
  return tryCatch(() => JSON.parse(s), reason => new Error(String(reason)))
}

Random values

Use case: an API that returns a non deterministic value.

Example: Math.random

Solution: IO

import { IO } from 'fp-ts/lib/IO'

const random: IO<number> = () => Math.random()

Synchronous side effects

Use case: an API that reads and/or writes to a global state.

Example: localStorage.getItem

Solution: IO

import { Option, fromNullable } from 'fp-ts/lib/Option'
import { IO } from 'fp-ts/lib/IO'

function getItem(key: string): IO<Option<string>> {
  return () => fromNullable(localStorage.getItem(key))
}

Use case: an API that reads and/or writes to a global state and may throw.

Example: readFileSync

Solution: IOEither, tryCatch

import * as fs from 'fs'
import { IOEither, tryCatch } from 'fp-ts/lib/IOEither'

function readFileSync(path: string): IOEither<Error, string> {
  return tryCatch(() => fs.readFileSync(path, 'utf8'), reason => new Error(String(reason)))
}

Asynchronous side effects

Use case: an API that performs an asynchronous computation.

Example: reading from standard input

Solution: Task

import { createInterface } from 'readline'
import { Task } from 'fp-ts/lib/Task'

const read: Task<string> = () =>
  new Promise<string>(resolve => {
    const rl = createInterface({
      input: process.stdin,
      output: process.stdout
    })
    rl.question('', answer => {
      rl.close()
      resolve(answer)
    })
  })

Use case: an API that performs an asynchronous computation and may reject.

Example: fetch

Solution: TaskEithertryCatch

import { TaskEither, tryCatch } from 'fp-ts/lib/TaskEither'

function get(url: string): TaskEither<Error, string> {
  return tryCatch(
    () => fetch(url).then(res => res.text()),
    reason => new Error(String(reason))
  )
}

Posted on Feb 12 '19 by:

Discussion

markdown guide
 

This is great, thank you. Is there library like Ramda for TS which hides internals of functional programming? Which embraces all of the common functions like in this article into one import statement. With FP-TS one has to know in which lib something belongs to to import it, can be time consuming.

 
 

This post was super helpful. Clear examples, and gives an overview of different alternatives. And I would never have thought that TaskEither existed unless reading this.