fp-ts不完全ガイド part1 Either

## はじめに

この文章は、fp-tsの使い方をわかりやすく説明し、利用者を増やすために書かれている。もともとは、会社で自分しか使わないであろうライブラリ、ツールをfp-tsを利用して作成していた。作成したツールが思った以上に利用されるようになり、メンテナンスできる人を増やす必要があった。そういうわけで後輩向け、同僚向けにわかりやすいfp-tsの解説を書く必要に迫られた。

僕はfp-tsの利用者が増えてほしい。fp(関数型言語を)一度使うと、もう使わなかった頃には戻れない。それくらい便利なのだが、しかし一度始めてしまうとなかなか抜けられない。やめられなくなるのだ。そのような、どうしようもないfp中毒患者を大量生産し、転職した際などにも好きにfp系の言語やライブラリが使えるようになると嬉しい。転職をたまに考えるのだが、もういまさら関数型以外で書く気がしない。それくらい関数型言語というのは面白い。

### Either
そういうわけで、part1はEitherである。

Eitherは、他の言語だとResult型と言ったりする。関数の成功と失敗を表すことができる。Rightが成功で、Leftが失敗である。（Result型だとOkとErrなどだったりする）

### parseInt

まずは`parseInt`について考えてみよう。JavaScriptで文字列をnumber型の値に変換する関数である。この関数は、成功したらnumber型の値を返す。失敗はしないが、parseできない場合はNaNを返す。（NaNとはNot-a-numberのことである。数字ではないという意味）

```ts
const value1 = Number.parseInt("4", 10); // -> 4
const value2 = Number.parseInt("a", 10); // -> NaN
```


TypeScriptでNaNはnumber型として扱われる。それをうっかり忘れていて、数字として扱ってしまったときに想定と違う動きをしてしまう。NaN + 1という処理は、型エラーにならない。1+NaNはNaNになる。そういうことのないように、parseIntを使用するときにはNaNチェックが必要になる。ただ、人間なのでうっかり忘れてしまうことがある。Either型を使用すれば、この問題が解決する。


parseIntEitherという関数を考える。

```ts
import * as E from "fp-ts/Either";
export const parseIntEither = (input: string): E.Either<string, number> => {
	const result = Number.parseInt(input, 10);
	// isNaNのときは、E.left("Invalid input")を返す。それ以外は、E.right(result)を返す。
	return Number.isNaN(result) ? E.left("Invalid input") : E.right(result);
};

const value1 = parseIntEither("42");
console.log(value1); // { _tag: 'Right', right: 42 }

const value2 = parseIntEither("abc");
console.log(value2); // { _tag: 'Left', left: 'Invalid input' }
```

  

これだけでは有用性がわからないかもしれない。

## Either型の値を利用する
  

それでは、実際に数字を足してみよう。Either型の値は、たとえばその値がnumberだったとしても、そのまま足し算などはできない。いろいろな方法で数字として扱うことができるようになる。まずはもっとも簡単なgetOrElseから見てみよう。

  

```ts
const rightValue = parseIntEither("42");
const value = E.getOrElse(() => 0)(rightValue);

const result = value + 100;
console.log({ result }); // { result: 142 }

const leftValue = parseIntEither("abc");
const value3 = E.getOrElse(() => 0)(leftValue);
const result2 = value3 + 100;
console.log({ result2 }); // { result2: 100 }
```

E.getOrElseは、Leftだったときにデフォルトの値を取り出すことが出来る。rightValueは42でparseに成功するので、100を足したら142になる。leftValueはabcでparseに失敗するため、デフォルト値の0になる。100を足したら100である。

  

他にもEitherを処理する方法がある。それはif文を使う方法である。

```ts
const process = () => {
	const rightValue = parseIntEither("42");
	if (E.isLeft(rightValue)) {
		return;
	}
	const add100Value = rightValue.right + 100;
	console.log(add100Value); // 142
};
```

E.isLeft, E.isRightがある。今回は関数の中で早期リターンを行い、それ以後ではrightValue.righで値を取り出すことができる。当然ながら三項演算子を使って同様のこともできる。

  

これくらいの処理であれば、わざわざEither型を導入せずとも自分でparseIntのwrapperを書けば良いかもしれない。isNaNでparse後の値を判定して、NaNのときは0を返すようにすれば良い。

  

Either型は、失敗する可能性のある関数を表現するのに便利である。


### JSONのparse

似た例だが、次はJSONをparseする関数を考えてみよう。JSON.parseは例外を投げる。しかし、例外をEitherとして処理をつづけることもできる。

  

```ts
import * as E from "fp-ts/Either";

const parseJsonEither = (jsonString: string): E.Either<string, unknown> =>
	E.tryCatch(
		() => JSON.parse(jsonString),
		(reason) => `ERROR:parseJsonEither__reason:${String(reason)}`,
	);

const objStr = `{"name": "John", "age": 30}`;
const obj = parseJsonEither(objStr);
console.log(obj); // { _tag: 'Right', right: { name: 'John', age: 30 } }

const invalidObjStr = "This is not a valid JSON string";
const invalidObj = parseJsonEither(invalidObjStr);
console.log(invalidObj);
/*
	{
  	_tag: 'Left',
  	left: 'ERROR:parseJsonEither__reason:SyntaxError: Unexpected token T in JSON at position 0'
	}
*/
```

  

parseJsonEither関数はstringを引数に取り、Eitherを返す。parse成功時はunknown、失敗したときにはerrorの理由をstringで返してくれる関数だ。E.tryCatchは2つの無名関数を取る。1つ目は、例外を投げる可能性のある処理。2つ目は、例外を投げたときの処理である。たとえば、redisなどのデータベースにstringで無理やりJSONとして保存されていたとしよう。そのJSONを取得してからparseする。そこでparseに失敗したら例外を発生せずに処理をつづけたい。そういうときに便利である。

  

それはtry catch節でも同じことができるのではないか、というのはその通りである。同じような処理を書くと以下のようになるだろう。

```ts
const parseJsonCatch = (json: string): unknown => {
	try {
		return JSON.parse(json);
	} catch (e) {
		return `ERROR:parseJsonCatch__reason:${String(e)}`;
	}
};
```

この関数の問題点はなんだろうか。それは、Errorが発生するのかどうか、外部からはわかりづらいことである。関数の返り値はunknownなので、結局あとでzodなどを用いてparseすることになるだろう。ただ、stringが返ってくるのかどうか、などを関数を利用する側からはわかりづらい。Either型は、この関数はErrorが発生するかもしれない、ということを利用者に教えてくれるのが利点である。

### pipe  

次はpipe処理について考えてみよう。pipeというのは、ある値があったとき、その値に順々に関数を適用していく処理だ。似ているけれど少し違うのが、JavaScriptの配列に存在するメソッドチェーンである。まずは簡単なメソッドチェーンの例を見てみよう。

```ts
const numbers = [1, 2, 3, 4, 5].map((n) => n * 2).filter((n) => n > 5);
console.log(numbers); // [ 6, 8, 10 ]
```

これがメソッドチェーンである。1,2,3,4,5という数字の配列に、与えられた数字を2倍にする関数を適用し、次に5より大きい数字にfilterする関数を適用している。似たようなことがfp-tsでもできる。少しEitherから話は逸れるけれど、fp-tsのArrayで、pipeを使ってメソッドチェーンと同じようなことができる。

```ts
import * as A from "fp-ts/Array";
const result = pipe(
	[1, 2, 3, 4, 5],
	A.map((n) => n * 2),
	A.filter((n) => n > 5),
);
console.log(result); // [ 6, 8, 10 ]
```

少し記述量が増えているけれども、このように処理が可能なのである。かなり端折った説明をすると、メソッドチェーンはオブジェクト指向プログラミング(OOP)スタイルで、pipeは関数型プログラミング（FP）のスタイルである。ここまでは、まだpipe処理の便利さがわかりづらいかもしれない。

  

pipe処理は配列だけではなく、Either型の際にも使用できる。すでにgetOrElseとif文で処理する方法を説明したが、次はmapを使う方法を紹介する。

### map関数を用いたEitherの処理


先にmapについて軽く説明する。mapとは、簡単に言うと関数を適用することである。たとえば配列にmapを使用すると、配列の要素ひとつずつに順番に関数を適用することができる。標準のJavaScriptでは配列のメソッドチェーンとして搭載されているが、fp-tsを利用することでEither型でもmapを使用することができる。FPではファンクタ（関手）と呼ぶことを記憶の片隅においておくと良いかもしれない。

  

mapを使った処理は、以下のようになる。

```ts
const parseIntEither = (input: string): E.Either<string, number> => {
	const result = Number.parseInt(input, 10);
	return Number.isNaN(result) ? E.left("Invalid input") : E.right(result);
};

const result = pipe(
	"42",
	parseIntEither, // parseIntEitherに成功するとRightに値が入り、失敗するとLeftにエラーメッセージが入る
	E.map((n) => n + 100), // Rightの場合のみ、値を変換し、 n + 100という関数を適用する。Leftの場合は何もしない
	E.map((n) => n / 2), // 上に続いて、Rightの場合はn / 2という関数を適用する
);
console.log(result); // Right 71

const result2 = pipe(
	"invalid input",
	parseIntEither,
	E.map((n) => n + 100), // この行は実行されない
	E.map((n) => n / 2), // この行は実行されない
);
console.log(result2); // Invalid input
```

  

すでにコメントでも書いているけれど、E.mapはrightのときだけ処理をすることができる。このmapを連鎖することができる。常に成功のときだけ値を扱えるというわけだ。

  

それでは、もう少し実践的な例を考えてみよう。

```ts
import * as E from "fp-ts/Either";
import { pipe } from "fp-ts/function";
import { z } from "zod";

// UserSchemaを定義
const userSchema = z.object({
	id: z.number(),
	name: z.string(),
	age: z.number(),
});

// JSON文字列をパースしてEither型に変換
const parseJsonEither = (json: string): E.Either<string, unknown> =>
	E.tryCatch(
		() => JSON.parse(json),
		(reason) => `ERROR:parseJsonEither__reason:${String(reason)}`,
	);

// Redisからユーザーリストを取得する関数（ダミー）
const getUserListFromRedis = (): string => {
	const fromRediString = `[
		{"id": 1, "name": "John", "age": 30},
		{"id": 2, "name": "Jane", "age": 25}
	]`;
	return fromRediString;
};

const result = pipe(
	getUserListFromRedis(), // Redisからユーザーリストを取得
	parseJsonEither, // JSON文字列をパースしてEither型に変換。失敗した場合はLeftにエラーメッセージが入る。leftのときは処理が止まり、flatMapの処理は実行されない
	E.flatMap((unknownJson) =>
		E.tryCatch(
			() => userSchema.array().parse(unknownJson),
			(reason) => `ERROR:parseUserSchema__reason:${String(reason)}`,
		),
	), // unknown型のJSONをUserSchemaを用いてparseを試みる。失敗した場合はLeftにエラーメッセージが入る
);
```

pipeの流れを追ってみよう。最初にgetUserListFromRedis関数が呼ばれる。実際のDBアクセスの際はTE型というものが使われるのだが、今回は説明のために簡略化する。絶対に失敗しない、userListを文字列で返す関数である。その文字列をparseJsonEitherでparseする。失敗したら、そこでpipeの処理は終了する。rightだった場合は、flatMapが実行される。flatMapとは、map処理のなかでさらにEitherを返せるものである(言語によってはchainとも言う。chainはflatMapのエイリアスとなっている)。少しややこしいのだが、今回はflatMapのなかでさらにEitherの処理が行われている。これはzodSchemaを利用してparseしている。tryCatchでparseに成功すればright, 失敗したらleftが返ってくる。もしflatMapではなくmapを使用すると、そのときのTypeScriptの型は

```ts
const result: E.Either<string, E.Either<string, {
    id: number;
    name: string;
    age: number;
}>>
```

このようになってしまう。Eitherのrightのなかに、さらにEitherがあるという非常にややこしく、使いづらい型になる。だから、入れ子になったEitherをflatにしてくれるflatMapを使用するのである。このあともmapを連鎖することができる。たとえば、抽出したユーザーリストから20歳以上に絞り込むと、以下のようになる。

```ts
const result = pipe(
	getUserListFromRedis(),
	parseJsonEither,
	E.flatMap((unknownJson) =>
		E.tryCatch(
			() => userSchema.array().parse(unknownJson),
			(reason) => `ERROR:parseUserSchema__reason:${String(reason)}`,
		),
	),
	E.map((userList) => userList.filter((user) => user.age >= 20)), // UserSchemaのparseに成功した場合、ageが20以上のユーザーのみを抽出
	E.mapLeft((err) => {
		sendSlackError(err); // エラーが発生した場合、Slackにエラーメッセージを送信
	}),
);
```

ついでにmapLeftについても説明する。これは通常のmapの逆で、Leftだったときに実行される関数である。たとえばsendSlackErrorという関数を定義しておいて、失敗したときにslackにエラーを送信したりできる。

  

仮に手続き型で書くと、以下のようになる。

```ts
const process = () => {
	const redisString = getUserListFromRedis();
	const jsonParsed = JSON.parse(fromRedisString);
	const schemaParsed = userSchema.array().parse(jsonParsed);
	const filtered = schemaParsed.filter((user) => user.age >= 20);
}
```

ただparseするだけなのに一時的な変数名を考えなければならない。さらにErrorがどこで発生するかわからない。エラー処理も別で書かなければならないが、try catchで捕まえる必要がある。どこでエラーが発生したのかはスタックトレースを追う必要がある。Either型であれば、leftの情報にどこでエラーが発生したのかを含めることも簡単だ。たとえばparseに失敗したときの入力値を出力するようにもできる。

  

いままでにEitherを利用する方法として、getOrElse, if文, mapなどを紹介した。最後にfold(match)を紹介する。

以下のようなReactのコンポーネントを考えてみよう。

```tsx
import * as E from "fp-ts/Either";
import { pipe } from "fp-ts/function";
import { z } from "zod";
const userSchema = z.object({
	id: z.number(),
	name: z.string(),
	age: z.number(),
});
type User = z.infer<typeof userSchema>;
export const UserListView = () => {
	const userListEither: E.Either<string, User> = useGetUserList();
	return pipe(
		userListEither,
		E.fold(
			(error) => <div>{error}</div>>,
			(userList) => (
				<div>
					<h1>User List</h1>
					<ul>
						{userList.map((user) => (
							<li key={user.id}>
								{user.name}({user.age})
							</li>
						))}
					</ul>
				</div>
			),
		),
	);
};
```

useGetUserListというカスタムフックがあり、どうやらEitherでLeftはstring, RightはUser\[\]が返ってくるようだ。このuserListをfoldで処理している。foldは2つの無名関数を取り、1つ目はLeftのときの処理、2つ目はRightのときの処理を書くことができる。foldは別の言語ではmatchということもある。実際にmatchはfoldのエイリアスとして、同じように使用することができる。余談になるが、僕はmatchをts-patternでよく使用していることもあり、正規表現のmatchでも使ったりするので、foldのほうが他のmatchと混ざらないので認知しやすいと思う。

  

もうひとつだけ、foldWを紹介する。実はfoldはLeftケース、Rightケースで同じ型しか返せない。foldWはLeftとRightで返り値を異なる型のものにできる。


```ts
const result = pipe(
	"42",
	parseIntEither,
	E.foldW(
		(error) => `${error}というエラーが発生した。`,
		(num) => num * 2,
	),
);
```

このとき、resultの型は string | numberとなる。

  

実はparseJSONについては自分で実装せずとも、fp-tsがすでに実装してくれている。parseJsonEitherの代わりにJ.parseという関数を使えば良い。

### テスト

Eitherを返す関数はテストが書きやすい。例外が発生するかどうかを判定しても良いけれど、Eitherにしておけばもっと簡単に書ける。LeftになるケースとRightになるケースのどちらも想定してテストを書けば良い。関数型言語の思想を取り入れると、徐々に純粋関数へと近づいていく。そうすると参照透過になり、非常にテストが書きやすくなるのである。
  

fp-tsのEitherについては以上である。完全とは言い難いが、重要な部分は説明できたかと思われる。次回はOption型について解説する。