この記事は ウェブクルー Advent Calendar 2022 19日目の記事です。
昨日は @wc_shibawan さんの「 Jacksonデシリアライズをいい感じにしたい話 」でした。
はじめに
fp-tsは、Typescriptで関数型プログラミングにおける型や手法を導入できるライブラリです。
https://github.com/gcanti/fp-ts
今回は、『fp-tsを使い始めてみたが、実装方法が今ひとつわからない』という方に、私がfp-tsを「完全に理解した 」となるまでに得られた知見を何点かご紹介できればと思います。
本記事を通して、少しでも実装パターンを掴んでいただけましたら幸いです。
私自身がScalaユーザーなので、一部Scalaと対比しながら進めます。
for-yield (≒ Do構文)
Scalaでいうfor-yield的な記述は、ぜひとも導入したいものです。
def getUserId(): Either[String, Int] = ???
def getUserName(id: Int): Either[String, String] = ???
val user: Either[String, (Int, String)] = for {
id <- getUserId()
name <- getUserName(id)
} yield (id, name)
とは言え、このシンタックスは当然ながらTypescriptには存在しません。
これを叶える為に、fp-tsでは独自の実装を提供しています。ありがたいですね。
import * as E from 'fp-ts/Either'
import { pipe } from 'fp-ts/function'
declare const getUserId: () => E.Either<string, number>
declare const getUserName: (id: number) => E.Either<string, string>
const user: E.Either<string, { id: number; name: string }> = pipe(
E.Do,
E.bind('id', () => getUserId()),
E.bind('name', ({ id }) => getUserName(id)),
E.map(({ id, name }) => ({ id, name })),
);
-
pipe関数の第1引数に渡す値で、処理全体の型が決まります。
fp-ts/Option
やfp-ts/Either
といった型毎のmoduleにあるDo
を利用できます。 -
bind関数の第1引数に、処理結果を束縛する変数名を指定できます。
後続のbindやmap関数で参照する事ができます。
非同期処理
Scalaでいう Future
です。
fp-tsにはPromiseを利用した Task
という型が用意されています。
interface Task<A> {
(): Promise<A>
}
Promiseを関数でラップする形になっている為、作成時には実行されません。
Scalaの Future
は即時評価で、作成と同時に処理が実行されてしまいますので、その点は異なります。
非同期処理 + Either
処理結果に成功or失敗の情報を持つ非同期処理は、多くの場面で扱われるものと思います。
Scalaなら Future[Either[A,B]]
という形のものです。
fp-tsでは Task
と Either
を組み合わせた TaskEtiher
という型が用意されています。
Either
を Task
で包む事で定義できます。
interface TaskEither<E, A> extends Task<Either<E, A>> {}
import * as E from 'fp-ts/Either';
import * as TE from 'fp-ts/TaskEither';
const fetchUser: TE.TaskEither<never, { id: number; name: string; }> = () => {
const value = E.of({ id: 1, name: 'john' });
return Promise.resolve(value);
}
Validation
複数の Either
を pipe, Do で順次処理する場合、最初に失敗したエラーが返却されます。
以下はその例です。
const validateName: (name: string) => E.Either<string, string> = (name) => pipe(
name,
E.fromPredicate((s) => s.length > 0, () => 'name is required.')
);
const validateAge: (age: number) => E.Either<string, number> = (age) => pipe(
age,
E.fromPredicate((n) => n >= 20, () => 'age must be at least 20 years old.')
);
const createUser = (name: string, age: number) => pipe(
E.Do,
E.bind('name', () => validateName(name)),
E.bind('age', () => validateAge(age)),
E.map(({ name, age }) => ({ name, age })),
)
console.log(createUser('john', 20)); // => Right<{ name: 'john', age: 20 }>
console.log(createUser('', 22)); // => Left<'name is required.'>
console.log(createUser('george', 18)); // => Left<'age must be at least 20 years old.'>
console.log(createUser('', 18)); // => Left<'name is required.'>
createUser('', 18)
は validateName
でエラーとなり、validateAge
は処理されません。
その為、全てのEitherを処理しつつ、全てのエラーの累積する事はできません。
複数のエラーを累積したい場合、Scalaの場合は Validation
を利用する方法があります。
(※標準APIではありませんが、catsやscalazで提供されています)
fp-tsでも Validation
を利用する事ができます。
ただし、型は用意されていませんので、わかりやすくする為に独自定義をおすすめします。
実態としては、エラーを累積させる為にLeftがArrayになった Either
です。
type Validation<E, A> = E.Either<E[], A>;
Either<E, A>
から Validation<E, A>
に変換するヘルパーメソッドも用意しましょう。
渡された Either
のLeft側をArrayでラップするだけです。
const lift: <E, A>(body: E.Either<E, A>) => Validation<E, A> = (body) => pipe(
body,
E.mapLeft((a) => [a]),
);
以上の準備が出来たら、下記の要領で、複数のエラーを累積させる事ができます。
const ap = E.getApplicativeValidation(A.getSemigroup<string>());
const createUser = (name: string, age: number) => Apply.sequenceS(ap)({
name: lift(validateName(name)),
age: lift(validateAge(age)),
});
console.log(createUser('john', 20)); // => Right<{ name: 'john', age: 20 }>
console.log(createUser('', 22)); // => Left<['name is required.']>
console.log(createUser('george', 18)); // => Left<['age must be at least 20 years old.']>
console.log(createUser('', 18)); // => Left<['name is required.','age must be at least 20 years old.']>
createUser('', 18)
が validateName
と validateAge
のエラーを両方保持しています。
入力フォームでのバリデーション処理など、検証によるエラー内容を一度に通知させたい場面で便利ですね。
最後に
fp-ts以外にも、Ramda や purify-ts など関数型プログラミングをサポートする為のライブラリは色々ありますので、別の機会に触ってみたいと思います
明日は @ayuko902ayuko さんの記事です。よろしくお願いします。