fp-tsのEqを使ってみる

## はじめに
TypeScriptを使ったフロントエンドの開発をしていると特定の条件下でのオブジェクト同士の等価性をチェックする必要がある場面が時々ある
もちろんその場で普通にベタ打ちによるチェックで対応することで何も問題なく対処が可能であるが、個人的に型クラスを使ってみる練習をしてみたいということもあり、[fp-ts]([GitHub - gcanti/fp-ts: Functional programming in TypeScript](https://github.com/gcanti/fp-ts))を使うとどうなるかというのをとりあえずメモ的に残して置きたいので投稿した

## 前提
* 型クラスについては知っている
* TypeScriptを使ったことがある

## 単純なオブジェクトの比較
以下のように`Point`型のプロパティの値が同じであるものが配列に含まれていることをチェックする場合

```typescript
type Point = {
  x: number;
  y: number;
};

const point1 = {
  x: 10,
  y: 10
};

const point2 = {
  x: 10,
  y: 20
};

const point3 = {
	x: 10,
	y: 10
};

[point1, point2].includes(point3);
// <- false

// 参考
point1 === point3
// <- false
```

- 単純にオブジェクトの中身が同じであっても`false`が返る
- この例において`true`を返すようにするには、各々のプロパティを比較するか文字列にして比較するなどして、自分で実装する方法があるが、今回はfp-tsの[Eq]([Getting started with fp-ts: Eq - DEV](https://dev.to/gcanti/getting-started-with-fp-ts-setoid-39f3))を使って実装してみたいと思う

## Eq
* fp-tsでは型クラスを`interface`によって定義されている
* シグニチャは以下

```typescript
export interface Eq<A> {
  readonly equals: (x: A, y: A) => boolean
}
```

* `Eq`型は `equals`のみを実装の要求がされている
* `number`、`string`、`boolean`などに対する`Eq`型のインスタンスは用意されている

```typescript
export declare const eqString: Eq<string>

export declare const eqNumber: Eq<number>

export declare const eqBoolean: Eq<boolean>
```

## `Eq`を使った関数の定義
* 単純に`Eq`を定義するだけだとあまり効果がない
* 大事なのは`Eq`のインスタンスが定義されれば例外なく適応できる関数を定義できることである
* 以下は`includes`を改良した例(`Eq`のドキュメントに載っているのをほとんどそのまま流用)

```typescript
const includes = <A>(E: Eq<A>) => (a: A, arr: Array<A>) => arr.some(item => E.equals(item, a));
```


## `Point`型に対する`Eq`の定義

### 愚直に定義してみる

```typescript
import { Eq } from "fp-ts/lib/Eq"

const eqPoint: Eq<Point> = ({
  equals: (p1, p2) => p1 === p2 || (p1.x === p2.x && p1.y === p2.y)
})
```

* これだけで一応定義できたことになる

### `getStructEq`を使う

* 今回は単純にプロパティの比較が実現できれば問題ないが如何せん手入力も多くバグの温床になりやすい
* その場合に`getStructEq<Uesr>`を使うと安全に`Eq<User>`のインスタンスが手に入る
* `getStructEq`のジェネリクスに渡した型の各々のプロパティに対して、任意の`Eq`のインスタンスを値とすると、戻り値の`Eq`のインスタンスの`equals`の実装は、各々のプロパティの`equals`を組み合わせた実装になっている
* 文章の説明よりコードを見たほうが理解しやすいので、以下例

```typescript
import { Eq, eqNumber, getStructEq } from "fp-ts/lib/Eq"

const eqPoint: Eq<Point> = getStructEq<Point>({
  x: eqNumber,
  y: eqNumber
})
```

## ネストしたオブジェクトの`Eq`の定義

* 今までの例では`Point`を使ってみたが、単純な型なのでネストのある型で考える

```typescript
import { getEq } from "fp-ts/lib/Array"
import { Eq, eqString, getStructEq } from "fp-ts/lib/Eq"

type User = {
  name: Name;
  friends: Pick<User, "name">[];
};

type Name = {
  firstName: string;
  lastName: string;
};

const eqName = getStructEq<Name>({
  firstName: eqString,
  lastName: eqString
});

const eqFriend: Eq<Pick<User, "name">> = getStructEq<Pick<User, "name">>({
  name: eqName
});

const eqFriends: Eq<Array<Pick<User, "name">>> = getEq(eqFriend);

const eqUser = getStructEq<User>({
  name: eqName,
  friends: eqFriends
});
```

* `eqName`自体は前の`eqPoint`と同じ感じだが、このインスタンス自体も当然`getStructEq`で組み合わせるのに使うことができる
* また特定の`Eq<A>`から`Eq<Array<A>>`は`fp-ts/lib/Array`モジュールの`getEq`関数から得ることができる

## 特定の値の等価から`Eq`を定義する
* 今まですべての値の等価をチェックしていたが、そこまで比較しなくても一意に定まるもので比較すれば十分である場合を考える

```typescript
type ToDo = {
  id: number;
  title: string;
  content: string;
};
```

* この場合`id`が同じであれば等価であると定義したい

### 愚直に定義してみる

```typescript
const eqToDo: Eq<ToDo> = {
  equals: (x, y) => x.id === y.id
};
```

### `contramap`を使って定義する

* `contramap`を使うともっと単純に`eqToDo`が得られる
* `contramap`のシグニチャは以下

```typescript
export declare const contramap: <A, B>(f: (b: B) => A) => (fa: Eq<A>) => Eq<B>
```

* `contramap`は型クラスを勉強していると出てくるが、定義したい`B`の`Eq`を関数`B => A`と`A`の`Eq`を与えることで、`A`の`Eq`に落とし込むことができる
* 自前での実装はおそらく以下の感じ

```typescript
import { Eq } from "fp-ts/lib/Eq"

const contramap = <A, B>(f: (b: B) => A) => (fa: Eq<A>): Eq<B> => ({
  equals: (x, y) => fa.equals(f(x), f(y))
})
```

* `eqToDo`の書き換えは以下

```typescript
import { contramap, eqNumber } from "fp-ts/lib/Eq"

const eqToDo = contramap<number, ToDo>(b => b.id)(eqNumber)
```

## 実際に`includes`を使ってみる

* ここまでで3つの`Eq`のインスタンスを定義してみたが、この`Eq`を使って前に定義した`includes`が適用できることを確認してみる

### `Point`

```typescript
// point1とpoint3の値が同じ
const point1 = {
  x: 10,
  y: 10
};

const point2 = {
  x: 100,
  y: 100
};

const point3 = {
  x: 10,
  y: 10
};

const points = [point1, point2];

includes(eqPoint)(point3, points);
// <- true
```

### `User`

```typescript
// user1とuser3の値が同じ
export const user1: User = {
  name: {
    firstName: "first",
    lastName: "last"
  },
  friends: [
    { name: { firstName: "first2", lastName: "last2" } },
    { name: { firstName: "first3", lastName: "last3" } }
  ]
};

export const user2: User = {
  name: {
    firstName: "first2",
    lastName: "last2"
  },
  friends: [
    { name: { firstName: "first", lastName: "last" } }
  ]
};

export const user3: User = {
  name: {
    firstName: "first",
    lastName: "last"
  },
  friends: [
    { name: { firstName: "first2", lastName: "last2" } },
    { name: { firstName: "first3", lastName: "last3" } }
  ]
};

const users = [user1, user2];

inclues(eqUser)(user3, users);
// <- true
```

### `ToDo`

```typescript
// toDo1とtoDo3のidが同じ
const toDo1: ToDo = {
  id: 1,
  title: "test",
  content: "foo"
};

const toDo2: ToDo = {
  id: 2,
  title: "test",
  content: "foo"
};

const toDo3: ToDo = {
  id: 1,
  title: "test2",
  content: "bar"
};

const toDoList = [toDo1, toDo2];

includes(eqToDo)(toDo3, toDoList);
// <- true
```

## 終わりに
個人的には`eqString.equal`を使うなど、わざわざプリミティブな型に対してはそこまでするする必要もないとは思うが、複雑な構造をした型に対しては常に意識して使ったほうがメリットがある場合は積極的に使っていきたい