DEV Community

Giulio Canti
Giulio Canti

Posted on • Updated on

Getting started with fp-ts: IO

In fp-ts a synchronous effectful computation is represented by the IO type, which is basically a thunk, i.e. a function with the following signature: () => A

interface IO<A> {
  (): A
}
Enter fullscreen mode Exit fullscreen mode

Note that IO represents a computation that never fails.

Examples of such computations are:

  • read / write to localStorage
  • get the current time
  • write to the console
  • get a random number

Example (read / write to localStorage)

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

const getItem = (key: string): IO<Option<string>> => () =>
  fromNullable(localStorage.getItem(key))

const setItem = (key: string, value: string): IO<void> => () =>
  localStorage.setItem(key, value)
Enter fullscreen mode Exit fullscreen mode

Example (get the current time)

const now: IO<number> = () => new Date().getTime()
Enter fullscreen mode Exit fullscreen mode

Example (write to the console)

const log = (s: unknown): IO<void> => () => console.log(s)
Enter fullscreen mode Exit fullscreen mode

Example (get a random number)

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

The IO type admits a Monad instance, so you can map...

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

/** get a random boolean */
const randomBool: IO<boolean> = io.map(random, n => n < 0.5)
Enter fullscreen mode Exit fullscreen mode

...or chain computations

/** write to the `console` a random boolean */
const program: IO<void> = io.chain(randomBool, log)

program()
Enter fullscreen mode Exit fullscreen mode

Note that nothing happens until you call program().

That's because program is a value which just represents an effectful computation, so in order to execute any side effect you must "run the IO action".

Since IO actions are just values you can use useful abstractions like Monoid to handle them...

Example (Dungeons and Dragons)

import { log } from 'fp-ts/Console'
import { getMonoid, IO, io } from 'fp-ts/IO'
import { fold, Monoid, monoidSum } from 'fp-ts/Monoid'
import { randomInt } from 'fp-ts/Random'

type Die = IO<number>

const monoidDie: Monoid<Die> = getMonoid(monoidSum)

/** returns the sum of the roll of the dice */
const roll: (dice: Array<Die>) => IO<number> = fold(monoidDie)

const D4: Die = randomInt(1, 4)
const D10: Die = randomInt(1, 10)
const D20: Die = randomInt(1, 20)

const dice = [D4, D10, D20]

io.chain(roll(dice), result => log(`Result is: ${result}`))()
/*
Result is: 11
*/
Enter fullscreen mode Exit fullscreen mode

..or define useful combinators

/** Log any value to the console for debugging purposes */
const withLogging = <A>(action: IO<A>): IO<A> =>
  io.chain(action, a => io.map(log(`Value is: ${a}`), () => a))

io.chain(roll(dice.map(withLogging)), result => log(`Result is: ${result}`))()
/*
Value is: 4
Value is: 2
Value is: 13
Result is: 19
*/
Enter fullscreen mode Exit fullscreen mode

Error handling

What if we want to represent a synchronous effectful computation that may fail?

We need two effects:

Type constructor Effect (interpretation)
IO<A> a synchronous effectful computation
Either<E, A> a computation that may fail

The solution is to put Either inside IO, which leads to the IOEither type

interface IOEither<E, A> extends IO<Either<E, A>> {}
Enter fullscreen mode Exit fullscreen mode

When we "run" a value of type IOEither<E, A>, if we get a Left it means that the computation failed with an error of type E, otherwise we get a Right which means that the computation succeeded with a value of type A.

Example (read a file)

Since fs.readFileSync may throw, I'm going to use the tryCatch helper

tryCatch: <E, A>(f: () => A) => IOEither<E, A>
Enter fullscreen mode Exit fullscreen mode

where f is a thunk that either throws an error (which is automatically catched by tryCatch) or returns a value of type A.

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

const readFileSync = (path: string): IOEither<Error, string> =>
  tryCatch(() => fs.readFileSync(path, 'utf8'), toError)

readFileSync('foo')() // => left(Error: ENOENT: no such file or directory, open 'foo')
readFileSync(__filename)() // => right(...)
Enter fullscreen mode Exit fullscreen mode

Lifting

The fp-ts/IOEither module provides other helpers which allow to create values of type IOEither, they are collectively called lifting functions.

Here's a summary

Start value lifting function
IO<E> leftIO: <E, A>(ml: IO<E>) => IOEither<E, A>
E left: <E, A>(e: E) => IOEither<E, A>
Either<E, A> fromEither: <E, A>(ma: Either<E, A>) => IOEither<E, A>
A right: <E, A>(a: A) => IOEither<E, A>
IO<A> rightIO: <E, A>(ma: IO<A>) => IOEither<E, A>

Example (loading a random file)

Let's say we want to randomly load the content of one of three files (1.txt, 2.txt, 3.txt).

The randomInt: (low: number, high: number) => IO<number> function returns a random integer uniformly distributed in the closed interval [low, high]

import { randomInt } from 'fp-ts/Random'
Enter fullscreen mode Exit fullscreen mode

We can chain randomInt with the readFileSync function defined above

import { ioEither } from 'fp-ts/IOEither'

const randomFile = ioEither.chain(
  randomInt(1, 3), // static error
  n => readFileSync(`${__dirname}/${n}.txt`)
)
Enter fullscreen mode Exit fullscreen mode

This doesn't type check though!

The types don't align: randomInt runs in the IO context while readFileSync runs in the IOEither context.

However we can lift randomInt to the IOEither context by using rightIO (see the summary above)

import { ioEither, rightIO } from 'fp-ts/IOEither'

const randomFile = ioEither.chain(rightIO(randomInt(1, 3)), n =>
  readFileSync(`${__dirname}/${n}.txt`)
)
Enter fullscreen mode Exit fullscreen mode

Top comments (17)

Collapse
 
crushjz profile image
Cesare Puliatti • Edited

Fantastic article, as always.

I'm pretty new to FP concepts, and I still need to learn how some simple imperative code translates to functional code.

How can I execute a side effect in a chain? EG:

some(myString)
  .chain(getCharactersA) // This returns an Option
  // Execute my side effect only if there is a value
Enter fullscreen mode Exit fullscreen mode

EDIT: I'm used to the tap operator of RxJS

Thank you!

Collapse
 
gcanti profile image
Giulio Canti • Edited

You return a (representation of a) side effect rather than execute a side effect.

Example

import { log } from 'fp-ts/lib/Console'
import { IO, io } from 'fp-ts/lib/IO'
import { chain, fold, none, Option, some } from 'fp-ts/lib/Option'
import { pipe } from 'fp-ts/lib/pipeable'

function getCharactersA(s: string): Option<string> {
  return s.length > 3 ? some(s.substring(3)) : none
}

function program(input: string): IO<void> {
  return pipe(
    some(input),
    chain(getCharactersA),
    // return my side effect only if there is a value
    fold(() => io.of(undefined), log)
  )
}

program('foo')() // no output
program('barbaz')() // outputs "baz" to the console
Enter fullscreen mode Exit fullscreen mode
Collapse
 
steida profile image
Daniel Steigerwald

Or fold(io.of(constVoid))

Collapse
 
sirmoustache profile image
SirMoustache

I like it but didn't' understand much.

Collapse
 
steida profile image
Daniel Steigerwald

The point of IO is to defer execution (making it lazy).
Deferring execution allows us to make pure functions from impure functions.
We prefer pure functions because they are easier to reason about.
The impure function has dependencies which are "tricky" because they are not deterministic.
It means, the impure function has side-effects and may return a different value or affects other functions.
Dangerous stuff better to avoid.
IO helps us to move a burden to the caller.

In Haskell like pure functional application, IO functions are only in composition root aka main.
Everything else (our app) is pure.

Watch this youtube.com/watch?v=cxs7oLGrxQ4&t=...

Collapse
 
vophanlee profile image
v0phan1ee

"IO helps us to move a burden to the caller."

This is weird. Why we need move a burden to caller. I mean this is different from the DI in OO programing: If we just use a thunk to wrap the "side effect", the side effect is just there, when caller call the function in main, every side effect just executes. i don't know what problems do IO solve.

Thread Thread
 
vophanlee profile image
v0phan1ee

DI in OO makes functions/class methods easy to be tested at least

Collapse
 
anotherhale profile image
Andy Hale

Great project! I was able to follow this IO tutorial and created a sample project that loads a file (synchronously) and parses the resulting YAML (github.com/anotherhale/fp-ts_sync-...). I am struggling with Tasks, dependent tasks specifically, that attempts the same file loading and parsing YAML but asynchronously (github.com/anotherhale/fp-ts_async...). Can anyone provide any feedback?

Collapse
 
davidchase profile image
David Chase

Excellent articles, I have been following along since the beginning (tcomb types articles) and FP-TS is groovy.

One thing I was curious about was what motivated you go into the direction of TS being the language of choice in comparison to some other compile to JS such as purescript, clojurescript, etc ? Just wondering about the origins.

Collapse
 
gcanti profile image
Giulio Canti

TypeScript allows me to reach for more people, my very goal is to spread fp concepts. TS is only a means, not an end.

Collapse
 
muzietto profile image
Marco Faustinelli • Edited

What is the difference between writing to the localStorage and writing to the DOM? Could you think a reasonably simple example where the effect is to activate reactjs to create some components into the DOM and listen to user input provided through them?

Do you think it is possible to construct a realistically useable FP-powered engine to handle interactions with the DOM without having to create a huge cathedral like the elm runtime? (which sometimes reminds me more of a prison than of a cathedral ;-)

Collapse
 
interferenc profile image
FaragÃģ MÃĄrton

I'd like to add just one thing: writing to localStorage CAN in fact throw a QuotaExceededError, so it is probably a better example for the use case for IOEither.

Collapse
 
nocnica profile image
Nočnica Mellifera

love to hear about edge cases! thanks.

Collapse
 
chautelly profile image
John

I would love to see a reactive example with mouse click events.

Collapse
 
gnomff_65 profile image
Timothy Ecklund

This is a great intro! I'm really excited to see async!

Collapse
 
wojciechmatuszewski profile image
Wojciech Matuszewski

Hi there,

I'm learning fp with this library and I'm wondering, is there a way to chain 3 or more IOEithers?
It seems like chain is statically typed to only allow 2 IOEithers

Thanks

Collapse
 
mateiadrielrafael profile image
Matei Adriel

You can use pipe (frok fp-ts/pipeable)