Redux-Saga와 함께 fp-ts TaskEither 모나드 사용

fp-ts TaskEither<L, R> monad은 성공 시 R 유형의 값을 반환하고 실패 시 L 유형의 값을 반환하는 비동기 계산을 나타내는 강력한 데이터 구조입니다. 예외(😖)를 처리하는 대신 더 "기능적"이고 투명한 방식으로 오류를 처리할 수 있습니다.

나는 최근에 Redux-Saga 내부에서 그것을 사용하고 있음을 발견했고 TaskEither 효과를 사용하여 call를 반환하는 함수를 실행했습니다. 이제 TaskEitherPromise 를 반환하는 함수일 뿐이므로 실제로 그렇게 하는 것은 매우 쉽습니다. 언뜻보기에 우리는 다음과 같이 할 수 있습니다.

// The service returning a `TaskEither`.
export function service(param: string): TE.TaskEither<Error, string> {
  return TE.right(`Hello World, ${param}`)
}

// The saga executing the service.
export function* saga() {
  // Either<Error, string>
  const result = yield* call(service('param')) 
}


The yield* operator here is used for typed-redux-saga, to obtain a better typing inside a Saga.



이런 식으로 result 는 실제로 Either<Error, string> 유형이지만 call 와 같은 인수를 전달하는 대신 함수를 직접 호출하여 call(service, 'param') 를 잘못 사용하고 있습니다. 이것은 Saga에 대한 단위 테스트를 작성할 때 문제를 일으킬 것입니다. 그렇다면 올바른 방식으로 서비스를 전달하면 어떻게 될까요? 그러면 다음과 같이 작성해야 합니다.

// TaskEither<Error, string>
const unexecutedResult = yield* call(service, 'param')

// Either<Error, string>
const result = yield* call(unexecutedResult)


이 경우 unexecutedResultTaskEither<Error, string> 유형이며 구체적인 결과를 얻으려면 실제로 실행해야 합니다. 이것은 작동하지만 단일 "작업"에 대해 두 가지call 효과를 테스트해야 합니다. 😾 정말 싫어요. 하지만 약간의 TypeScript 마법을 사용하여 단일 호출로 TaskEither를 만들고 실행하는 유틸리티 함수를 작성하여 이를 개선할 수 있습니다.

/**
 * Utility to evaluate a function returning a `TaskEither`.
 */
function* callTaskEither<
  L,
  R,
  Fn extends (...genericArgs: Array<any>) => TE.TaskEither<L, R>,
>(fn: Fn, ...args: Parameters<Fn>): SagaGenerator<E.Either<L, R>> {
  const task: TE.TaskEither<L, R> = yield* call(fn, ...args)
  return yield* call(task)
}

/**
 * Evaluate a one-argument function returning a TaskEither.
 */
export function callTaskEither1<L, R, P1>(
  fn: (p1: P1) => TE.TaskEither<L, R>,
  p1: P1,
): SagaGenerator<E.Either<L, R>> {
  return callTaskEither(fn, p1)
}

/**
 * Helper type for `callTaskEither1`.
 */
export type TCallTaskEither1<L, R, P1> = (
  fn: (p1: P1) => TE.TaskEither<L, R>,
  p1: P1,
) => SagaGenerator<E.Either<L, R>>


함수callTaskEither는 앞에서 설명한 두 가지call를 수행하는 생성기일 뿐입니다. TypeScript는 스프레드 연산자를 사용하여 함수의 매개변수를 유추하는 데 문제가 있으므로 필요한 각 매개변수 수에 대해 수동으로 도우미 함수와 유형을 만들어야 합니다. 최고는 아니지만 일회성 작업입니다! 이 힘든 작업을 통해 다음과 같이 단일call 효과를 사용하여 작업 유형 검사를 할 수 있습니다.

// Either<Error, string>
const result = yield* call<TCallTaskEither1<Error, string, string>>(
  callTaskEither1,
  service,
  'param',
)


여전히 약간 장황하지만 실제로 모든 곳에서 두 가지call 효과를 사용하는 것보다 선호합니다!

봐요 🤠

좋은 웹페이지 즐겨찾기