How to work with non-functional code using fp-ts
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, tryCatch2v
import { Either, tryCatch2v } from 'fp-ts/lib/Either'
function parse(s: string): Either<Error, unknown> {
return tryCatch2v(() => 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> = new IO(() => 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 new IO(() => fromNullable(localStorage.getItem(key)))
}
- Use case
- an API that reads and/or writes to a global state and may throw.
- Example
readFileSync
- Solution
- IOEither, tryCatch2v
import * as fs from 'fs'
import { IOEither, tryCatch2v } from 'fp-ts/lib/IOEither'
function readFileSync(path: string): IOEither<Error, string> {
return tryCatch2v(() => 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 Task(
() =>
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
- TaskEither, tryCatch
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)))
}
Playground
Check out this repo by Tycho Tatitscheff containing the source code and tests.
This content was originally published as a blog post on February 12, 2019.