Scalaユーザーが使うfp-ts

この記事は ウェブクルー Advent Calendar 2022 19日目の記事です。
昨日は @wc_shibawan さんの「 [Jacksonデシリアライズをいい感じにしたい話](https://qiita.com/wc_shibawan/items/a2a5a33dc09b5429d2af) 」でした。

# はじめに

fp-tsは、Typescriptで関数型プログラミングにおける型や手法を導入できるライブラリです。
https://github.com/gcanti/fp-ts

今回は、『fp-tsを使い始めてみたが、実装方法が今ひとつわからない』という方に、私がfp-tsを「完全に理解した :upside_down: 」となるまでに得られた知見を何点かご紹介できればと思います。

本記事を通して、少しでも実装パターンを掴んでいただけましたら幸いです。
私自身がScalaユーザーなので、一部Scalaと対比しながら進めます。

## for-yield (≒ Do構文)

Scalaでいうfor-yield的な記述は、ぜひとも導入したいものです。

```scala:scala
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では独自の実装を提供しています。ありがたいですね。

```typescript:typescript
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` という型が用意されています。

```typescript
interface Task<A> {
  (): Promise<A>
}
```

Promiseを関数でラップする形になっている為、作成時には実行されません。
Scalaの `Future` は即時評価で、作成と同時に処理が実行されてしまいますので、その点は異なります。

## 非同期処理 + Either

処理結果に成功or失敗の情報を持つ非同期処理は、多くの場面で扱われるものと思います。
Scalaなら `Future[Either[A,B]]` という形のものです。

fp-tsでは `Task` と `Either` を組み合わせた `TaskEtiher` という型が用意されています。
`Either` を `Task` で包む事で定義できます。

```typescript
interface TaskEither<E, A> extends Task<Either<E, A>> {}
```

```typescript
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 で順次処理する場合、最初に失敗したエラーが返却されます。
以下はその例です。

```typescript
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` です。

```typescript
type Validation<E, A> = E.Either<E[], A>;
```

`Either<E, A>` から `Validation<E, A>` に変換するヘルパーメソッドも用意しましょう。
渡された `Either` のLeft側をArrayでラップするだけです。

```typescript
const lift: <E, A>(body: E.Either<E, A>) => Validation<E, A> = (body) => pipe(
  body,
  E.mapLeft((a) => [a]),
);
```

以上の準備が出来たら、下記の要領で、複数のエラーを累積させる事ができます。

```typescript
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](https://ramdajs.com/) や [purify-ts](https://gigobyte.github.io/purify/) など関数型プログラミングをサポートする為のライブラリは色々ありますので、別の機会に触ってみたいと思います :smiley:

明日は @ayuko902ayuko さんの記事です。よろしくお願いします。