DEV Community

Timothy Ecklund
Timothy Ecklund

Posted on

fp-ts, sequenceT, and sweet sweet async typed FP

Recently I found the need to make a set of async calls with different return types. This is a pretty common task, we'd like to make some calls in parallel and collect the results once everything is done. Let's take a look at the docs for fp-ts async tasks.

const tasks = [task.of(1), task.of(2)]
array
  .sequence(task)(tasks)()
  .then(console.log) // [ 1, 2 ]

Hmm. Well that's pretty nice, but what happens when the types are different?

const tasks = [T.task.of(1), T.task.of("hello")]
array
  .sequence(task)(tasks)()
  .then(console.log) // [1, "hello"] I hope?

Uh oh.
Sequence Type Error

Well darn. The type of sequence is (simplified) Array[F[A]] => F[Array[A]], so all the return types would have to be the same.

What do? :/

After some googleing, I ran across the magical sequenceT.

 /** 
 * const sequenceTOption = sequenceT(option)
 * assert.deepStrictEqual(sequenceTOption(some(1)), some([1]))
 * assert.deepStrictEqual(sequenceTOption(some(1), some('2')), some([1, '2']))
 * assert.deepStrictEqual(sequenceTOption(some(1), some('2'), none), none)
 */

Nice! Ok, let's try it out.

import * as T from 'fp-ts/lib/Task'
import { sequenceT } from 'fp-ts/lib/Apply'
import { pipe } from 'fp-ts/lib/pipeable'

pipe(
  sequenceT(T.task)(T.of(42), T.of("tim")), //[F[A], F[B]] => F[A, B] 
  T.map(([answer, name]) => console.log(`Hello ${name}! The answer you're looking for is ${answer}`))
)();
Hello tim! The answer you're looking for is 42

Well that's rad. pipe allows us to chain calls together, so the result of sequenceT is passed in to T.map. T.map destructures the tuple and we can do as we please with some guarantees about our data. But what if our Tasks can fail?

pipe(
  sequenceT(TE.taskEither)(TE.left("no bad"), TE.right("tim")),
  TE.map(([answer, name]) => console.log(`Hello ${name}! The answer you're looking for is ${answer}`)),
  TE.mapLeft(console.error)
)();
no bad

Awesome! Ok, time to get fancy with it. What if we are actually making some calls to an API, and we want to ensure that the results we get from the API conform to the expected schema?

Let's try it out by hitting a dummy REST endpoint with axios, which is a handy http client.

import { array } from 'fp-ts/lib/Array'
import axios, { AxiosResponse } from 'axios';
import * as t from 'io-ts'

//create a schema to load our user data into
const users = t.type({
  data: t.array(t.type({
    first_name: t.string
  }))
});

//schema to hold the deepest of answers
const answer = t.type({
  ans: t.number
});

//Convert our api call to a TaskEither
const httpGet = (url:string) => TE.tryCatch<Error, AxiosResponse>(
  () => axios.get(url),
  reason => new Error(String(reason))
)

/**
 * Make our api call, pull out the data section and decode it
 * We need to massage the Error type, since `decode` returns a list of `ValidationError`s
 * We should probably use `reporter` to make this nicely readable down the line
 */
const getUser = pipe(
  httpGet('https://reqres.in/api/users?page=1'),
  TE.map(x => x.data),
  TE.chain((str) => pipe(
    users.decode(str), 
    E.mapLeft(err => new Error(String(err))), 
    TE.fromEither)
  )
);

const getAnswer = pipe(
  TE.right(42),
  TE.chain(ans => pipe(
    answer.decode({ans}), 
    E.mapLeft(err => new Error(String(err))), 
    TE.fromEither)
  )
)

/**
 * Make our calls, and iterate over the data we get back
 */
pipe(
  sequenceT(TE.taskEither)(getAnswer, getUser),
  TE.map(([answer, users]) => array.map(users.data, (user) => console.log(`Hello ${user.first_name}! The answer you're looking for is ${answer.ans}`))),
  TE.mapLeft(console.error)
)();
Hello George! The answer you're looking for is 42
Hello Janet! The answer you're looking for is 42
Hello Emma! The answer you're looking for is 42
Hello Eve! The answer you're looking for is 42
Hello Charles! The answer you're looking for is 42
Hello Tracey! The answer you're looking for is 42

Heck yeah! We did it! Async Typed FP for everyone! :)

Discussion (7)

Collapse
ybogomolov profile image
Yuriy Bogomolov

Awesome! I sometimes use this trick to sequence foldables with different types:

function combineOption<A, B, C, D>(a: Option<A>, b: Option<B>, c: Option<C>, d: Option<D>): Option<[A, B, C, D]>;
function combineOption<A, B, C>(a: Option<A>, b: Option<B>, c: Option<C>): Option<[A, B, C]>;
function combineOption<A, B>(a: Option<A>, b: Option<B>): Option<[A, B]>;
function combineOption<T>(...list: Array<Option<T>>): Option<T[]> {
  return array.sequence(option)(list);
}
Collapse
giogonzo profile image
Giovanni Gonzaga

Isn't this just combineOptions = sequenceT(option)?

Collapse
ybogomolov profile image
Yuriy Bogomolov

Pretty much like it, yes. Although I should admit that sequenceT was introduced in fp-ts@2, while array.sequence works for fp-ts@1 as well.

Collapse
wayneseymour profile image
Tre'

Async typed fp for the win! Good example!

Collapse
vassilispallas profile image
Vassilis Pallas

Awesome! Is it possible to concatenate all the errors into one array? Because mapLeft contains only the last error.

Collapse
gnomff_65 profile image
Timothy Ecklund Author

Absolutely! Please check out my follow up article dev.to/gnomff_65/fp-ts-and-beautif...