[小ネタ] [fp-ts] TaskEitherで条件付きリトライを実装する

2020.04.30

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

前回の記事の続きの小ネタとなりますが、taskEitherで条件付きリトライを実装してみたのでそのメモです。

指針

以下のようにTaskEitherとリトライ条件の判定関数を受け取ってリトライ付きで実行する関数を実装してみます。

このシグネチャにすることでWeb APIのレスポンスやDBエラーを柔軟にハンドリングでき、リトライ処理を透過的に追加することができます。

function retry <E, R>(fa: () => TaskEither<E, R>, predicate: (e:E => boolean)):TaskEither<E, R>

leftFlatMap

まずはEitherのLeft側だけをflatMapする以下のヘルパーを定義します。

import { taskEither as TE } from 'fp-ts'
import { TaskEither } from 'fp-ts/lib/TaskEither'

export const leftFlatMap = <R, A, B>(f: (a: A) => TaskEither<B, R>) => (fa: TaskEither<A, R>): TaskEither<B, R> =>
  TE.fold(
    (a:A) => f(a),
    (r:R) => TE.right(r)
  )(fa)

retry

上記を使って実装すると下記のようになります。

import { TaskEither, fromIOEither, left } from 'fp-ts/lib/TaskEither'

function retry<E, A> (fa: () => TaskEither<E, A>, predicate: (e: E) => boolean): TaskEither<E, A> {
  return pipeable.pipe(
    fa(),
    leftFlatMap(e => predicate(e) ? retry(fa, predicate) : left(e))
  )
}

使ってみます

import { fromIOEither } from 'fp-ts/lib/TaskEither'
import { leftFlatMap } from './util/flatmap'
import { pipeable, either } from 'fp-ts'

const Retry = 'Retry'
const Abort = 'Fail'

type Fail = typeof Retry | typeof Abort

let count = 0

const t = () => fromIOEither(() => {
  console.log(`Retry count ${count}`)
  count++
  return either.left<Fail, string>(count >= 10000 ? Fail : Retry)
})

retry(t, e => e === Retry)().then(console.log)
// Retry count 0
// Retry count 1
// Retry count 2
// .
// .
// .
// Retry count 9999
// { _tag: 'Left', left: 'Abort' }

retry-ts

実装してから気づいたのですが、ほぼ同じインターフェースでバックオフによるスリープを追加したretry-tsライブラリがありました。こちらを使ったほうがいいと思います・・・。

まとめ

fp-tsでリトライ処理を実装してみました。