🤢

fp-tsで単一の項目について並行的にバリデーションをかけたいときの書き方

2023/08/23に公開1

事前に目をとおすとよいもの

https://dev.to/gcanti/getting-started-with-fp-ts-either-vs-validation-5eja (APIが古い & Eitherの解説だけど大筋で参考になる)
https://js.excelspeedup.com/fp-ts-taskeither-sequence (比較的新しい、パターンが色々あって説明も親切、TaskEitherなのでおすすめ)

以上を踏まえた上で、筆者独自の解釈込みで色々と書いているので鵜呑みは危険です。

準備

まずは必要なモジュールのimportです。

import {pipe} from 'fp-ts/lib/function';
import * as T from 'fp-ts/lib/Task';
import * as TE from 'fp-ts/lib/TaskEither';
import * as RArr from 'fp-ts/ReadonlyArray';

次に、便利のために必要なパーツを実装していきましょう。
必要なものは:

  • 型エイリアス
  • EitherをValidationへ持ち上げるヘルパー
  • バリデーションの並行実行のためのApplicative取得処理のヘルパー
    です。
export type TaskValidation<E, A> = TaskEither<ReadonlyArray<E>, A>;

export function lift<E, A>(a: TaskEither<E, A>) {
  return pipe(
    a,
    TE.mapLeft((e): ReadonlyArray<E> => [e]),
  );
}

export function getParallelTaskValidationApplicative<T>() {
  // fp-tsはmonoidをさらにsemigroupという概念に分類してるっぽいけど大体同じものです(たぶん)
  return TE.getApplicativeTaskValidation(T.ApplyPar, RArr.getSemigroup<T>());
}

はい、できました。
ではバリデーション処理を書いていきましょう

バリデーション処理実践

バリデーション関数を用意します。

const isLen1 = TE.fromPredicate(
  (txt: string) => txt.length === 1,
  (v) => ({v, msg: 'not length 1'}),
);

const isLen2 = TE.fromPredicate(
  (txt: string) => txt.length === 2,
  (v) => ({v, msg: 'not length 2'}),
);

const isLen3 = TE.fromPredicate(
  (txt: string) => txt.length === 3,
  (v) => ({v, msg: 'not length 3'}),
);

ゴミみたいなイグザンプルが生成されましたね。
めんどいので今回中身はただの同期処理ですが、TEなので当然非同期処理にも対応しています。

愚直ver

愚直に書くならこうなるでしょう

type Err = {v: string; msg: string};
const seqPar = App.sequenceT(getParallelTaskValidationApplicative<Err>());
const v = 'I my me';
const resultGuchoku = seqPar(
  lift(isLen1(v)),
  lift(isLen2(v)),
  lift(isLen2(v)),
);

これで 'not length [1|2|3]' が収集できます。やったね。
でもなんかいやですね。 v に対して複数のバリデーターを適用する、みたいなそういう書き方で攻めたい。そういう日もあるとおもいます。

flapを使う

Functorのもつ、flapという実装を使ってみましょう。

const validators = [isLen1, isLen2, isLen3];
const resultFlap = seqPar(
  pipe(RArr.flap(v)(validators), RArr.map(lift))
);

いいですね。 v 単体に対してvalidatorsを適用している感じが出ています。

apを使う

Applyというクラスがapという実装を提供してくれています。
fp-tsにおいて、ApplicativeはApplyを継承したものという形みたいです。他がどうなのかとかはよくわかりません。

const resultAp = seqPar(pipe(validators, RArr.ap([v]), RArr.map(lift)));

これも悪くないですね。
セマンティクスが微妙に違うのと、apを使ったものは同一の型であれば複数の値についてvalidatorsを網羅的に適用できるので汎用性はこちらのほうが高そうです。

flapとapのパターンはどちらも処理の抽象度が高いので type applyValidationPar = < A>(value: A) => <E>(validators: ReadonlyArray<(v: A) => E>) => TaskValidation<E, A> というようなシグネチャのユーティリティ関数として宣言、再利用ができそうです。
よかったですね。

まとめ

  • 標準のPromiseだけでバリデーション処理を宣言的に書こうとすると綺麗に書くのが難しかったりするのでfp-tsは結構パワフル
  • 昔はValidationっていう型クラスを用意してくれていた痕跡があるのに削除されている
  • fp-tsを調べるときは、fp-tsのドキュメントを読むよりPureScriptについて調べたほうが早い説

Discussion

nap5nap5

値を複数個の関数の引数にアプライするflapを使って、少しデモを書いてみました。

validatorsをpipeに乗せてから、flapで引数をアプライした後、バリデーション結果のうちエラーがあれば、leftで、なければ、rightでリターンしています。

type ValidationMessageFormat = {
  message: string;
  inputData: unknown;
  fn: string;
};

type Validators = Array<(a: any) => E.Either<ValidationMessageFormat, any>>;

export const validateAll = (validators: Validators) => (input: unknown) =>
  pipe(validators, A.flap(input), A.lefts, (result) =>
    result.length > 0 ? E.left(result) : E.right(input)
  );

demo code.
https://codesandbox.io/p/sandbox/quirky-panka-csjm6v?file=/src/index.ts:1,39