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?
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! :)
Top comments (6)
Awesome! I sometimes use this trick to sequence foldables with different types:
Isn't this just
combineOptions = sequenceT(option)
?Pretty much like it, yes. Although I should admit that
sequenceT
was introduced in fp-ts@2, whilearray.sequence
works for fp-ts@1 as well.Async typed fp for the win! Good example!
Awesome! Is it possible to concatenate all the errors into one array? Because
mapLeft
contains only the last error.Absolutely! Please check out my follow up article dev.to/gnomff_65/fp-ts-and-beautif...