fp-ts, sequenceT 및 달콤한 달콤한 비동기 유형 FP

최근에 다른 반환 유형을 사용하여 일련의 비동기 호출을 만들어야 할 필요성을 발견했습니다. 이것은 매우 일반적인 작업입니다. 병렬로 몇 가지 호출을 수행하고 모든 작업이 완료되면 결과를 수집하고 싶습니다. fp-ts async tasks 에 대한 문서를 살펴보겠습니다.

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

흠. 꽤 좋은데 유형이 다르면 어떻게 됩니까?

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

어 오.


젠장. sequence 의 유형은 (단순화된) Array[F[A]] => F[Array[A]] 이므로 모든 반환 유형이 동일해야 합니다.

무엇을? :/

약간의 구글 검색 후 마법의 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)
 */

멋진! 좋아, 한번 해 보자.

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

글쎄요. pipe를 사용하면 호출을 함께 연결할 수 있으므로 sequenceT의 결과가 T.map로 전달됩니다. T.map는 튜플을 분해하고 데이터에 대한 몇 가지 보증을 통해 원하는 대로 할 수 있습니다. 하지만 작업이 실패할 수 있다면 어떻게 될까요?

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

대박! 좋아, 멋져질 시간이야. 실제로 API를 호출하고 API에서 얻은 결과가 예상 스키마를 준수하는지 확인하려면 어떻게 해야 할까요?

편리한 http 클라이언트인 axios 로 더미 REST 끝점을 쳐서 사용해 봅시다.

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

그래! 우리는 해냈다! 모두를 위한 비동기식 FP! :)

좋은 웹페이지 즐겨찾기