見出し画像

fp-tsライブラリを使った部分欠損した状態のデータ構造について

こんにちわ。nap5です。

fp-tsライブラリを使った部分欠損した状態のデータ構造について紹介したいと思います。



たとえば、5個アイテムがあってそのうち偶数番目のアイテムだけエラーが起きた場合を考慮します。


import { z } from "zod";
import { TaskEither } from "fp-ts/lib/TaskEither";
import { tryCatch } from "fp-ts/lib/TaskEither";
import { sequenceT } from "fp-ts/lib/Apply";
import { ApplyPar } from "fp-ts/lib/Task";

const CustomErrorDataSchema = z.custom<CustomError>();
type CustomErrorData = z.infer<typeof CustomErrorDataSchema>;
type UserId = number;

class CustomError extends Error {
  constructor(message: string, option?: { cause: unknown }) {
    super(message, option);
  }
}

const doN = (n: number): TaskEither<CustomErrorData, UserId> => {
  return tryCatch(
    async () => {
      if (n % 2 === 0) {
        return Promise.reject(
          new CustomError(`Something went wrong... [${n}]`)
        );
      }
      return n;
    },
    (e) => e as CustomErrorData
  );
};

(async () => {
  const results = await sequenceT(ApplyPar)(
    doN(1),
    doN(2),
    doN(3),
    doN(4),
    doN(5)
  )();
  console.log(results)
})();


実行結果になります。2番目のアイテムと4番目のアイテムがこけていることがわかります。エラーが途中で起きていても、早期終了するのではなく、順次処理をしているのがポイントです。

[
  { _tag: 'Right', right: 1 },
  {
    _tag: 'Left',
    left: CustomError: Something went wrong... [2]
        at /home/someone/wrksp/src/1.ts:22:11
        at /home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:229:42
        at step (/home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:56:23)
        at Object.next (/home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:37:53)
        at /home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:31:71
        at new Promise (<anonymous>)
        at __awaiter (/home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:27:12)
        at /home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:223:33
        at async Promise.all (index 1)
  },
  { _tag: 'Right', right: 3 },
  {
    _tag: 'Left',
    left: CustomError: Something went wrong... [4]
        at /home/someone/wrksp/src/1.ts:22:11
        at /home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:229:42
        at step (/home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:56:23)
        at Object.next (/home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:37:53)
        at /home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:31:71
        at new Promise (<anonymous>)
        at __awaiter (/home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:27:12)
        at /home/someone/wrksp/node_modules/fp-ts/lib/TaskEither.js:223:33
        at async Promise.all (index 1)
  },
  { _tag: 'Right', right: 5 }
]


これをもとにサマリと明細のデータ構造にそろえてやると、バッチ処理のようなインターフェースを持つプログラムが作りやすくなります。これを達成するためにcollect関数を正常系と異常系の2つ用意します。

import { getSemigroup } from "fp-ts/lib/Array";
import { Either, isLeft as isError } from "fp-ts/lib/Either";

const collectError = <E>(results: Either<E, unknown>[]) => {
  const S = getSemigroup<E>();
  const errors = results
    .map((result) => {
      if (isError(result)) {
        return result.left;
      }
    })
    .filter((item): item is E => !!item);
  return S.concat(errors, []);
};

const collectData = <A>(results: Either<unknown, A>[]) => {
  const S = getSemigroup<A>();
  const data = results
    .map((result) => {
      if (!isError(result)) {
        return result.right;
      }
    })
    .filter((item): item is A => !!item);
  return S.concat(data, []);
};


これらを使うと以下のような出力結果を作りやすくなります。
pretty-formatライブラリを使ってNeatな感じに出力してます。


$ yarn do-2
Object {
  "detail": Object {
    "data": Array [
      1,
      3,
      5,
    ],
    "errors": Array [
      [Error: Something went wrong... [2]],
      [Error: Something went wrong... [4]],
    ],
    "results": Array [
      Object {
        "_tag": "Right",
        "right": 1,
      },
      Object {
        "_tag": "Left",
        "left": [Error: Something went wrong... [2]],
      },
      Object {
        "_tag": "Right",
        "right": 3,
      },
      Object {
        "_tag": "Left",
        "left": [Error: Something went wrong... [4]],
      },
      Object {
        "_tag": "Right",
        "right": 5,
      },
    ],
  },
  "summary": Object {
    "dataCount": 3,
    "errorCount": 2,
    "resultCount": 5,
  },
}


これを使うとタグがLeftの場合、つまり異常が起きたアイテムのみリカバリ処理のUIを実現するためのアプローチが取りやすくなります。


デモコードです。



簡単ですが、以上です。

この記事が気に入ったらサポートをしてみませんか?