DEV Community

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/Option'

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

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/Option'

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

Exceptions

Use case: an API that may throw.

Example: JSON.parse

Solution: Either, tryCatch

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

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

Random values

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

Example: Math.random

Solution: IO

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

const random: IO<number> = () => Math.random()
Enter fullscreen mode Exit fullscreen mode

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/Option'
import { IO } from 'fp-ts/IO'

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

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/IOEither'

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

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/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)
    })
  })
Enter fullscreen mode Exit fullscreen mode

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

Example: fetch

Solution: TaskEither, tryCatch

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

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

Discussion (6)

pic
Editor guide
Collapse
mk0y8 profile image
Marko Jakic

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.

Collapse
leohxj profile image
Leo Hui

I want too.

Collapse
area73 profile image
Rodrigo

Ciao Giulio, I'm now starting to play with your awesome library and there are a couple of things that I just don't get it right , probably because of my poor knowledge in FP.

The first think that surprise me is how to implement the pipe function, for other libraries that I used before pie was defined like:
pipe :: ((a -> b), ..., (y -> z)) -> a -> z

So as an example I'm use to do something like:

const process = pipe(
map(toUpperCase),
map(removeVocals),
map(countChar)
);
Enter fullscreen mode Exit fullscreen mode

And then later pass my data type (IO, Option or any functor) to evaluate the result


const result = process(customOption)  

Enter fullscreen mode Exit fullscreen mode

but in your library you define it like:
pipe :: (a, (a -> b), ..., (y -> z)) -> z

so I don't see the way to get partially apply in order to do lazy evaluation and I also have to pass the data type as a first argument witch is really strange for me to be in a pipe.

Can you point me to the right direction?

Collapse
gcanti profile image
Giulio Canti Author

see flow gcanti.github.io/fp-ts/modules/fun...

  • pipe: pipes the value of an expression into a pipeline of functions
  • flow: performs left-to-right function composition
Collapse
marcusnielsen profile image
Marcus Nielsen

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.

Collapse
aepari profile image
ANEESH EPARI • Edited

Apologies, I'm new to JS and TS. Where have we declared type classes for Lazy and Some or is it in native js?