4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

はじめに

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

前提

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

単純なオブジェクトの比較

以下のようにPoint型のプロパティの値が同じであるものが配列に含まれていることをチェックする場合

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)を使って実装してみたいと思う

Eq

  • fp-tsでは型クラスをinterfaceによって定義されている
  • シグニチャは以下
export interface Eq<A> {
  readonly equals: (x: A, y: A) => boolean
}
  • Eq型は equalsのみを実装の要求がされている
  • numberstringbooleanなどに対するEq型のインスタンスは用意されている
export declare const eqString: Eq<string>

export declare const eqNumber: Eq<number>

export declare const eqBoolean: Eq<boolean>

Eqを使った関数の定義

  • 単純にEqを定義するだけだとあまり効果がない
  • 大事なのはEqのインスタンスが定義されれば例外なく適応できる関数を定義できることである
  • 以下はincludesを改良した例(Eqのドキュメントに載っているのをほとんどそのまま流用)
const includes = <A>(E: Eq<A>) => (a: A, arr: Array<A>) => arr.some(item => E.equals(item, a));

Point型に対するEqの定義

愚直に定義してみる

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を組み合わせた実装になっている
  • 文章の説明よりコードを見たほうが理解しやすいので、以下例
import { Eq, eqNumber, getStructEq } from "fp-ts/lib/Eq"

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

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

  • 今までの例ではPointを使ってみたが、単純な型なのでネストのある型で考える
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を定義する

  • 今まですべての値の等価をチェックしていたが、そこまで比較しなくても一意に定まるもので比較すれば十分である場合を考える
type ToDo = {
  id: number;
  title: string;
  content: string;
};
  • この場合idが同じであれば等価であると定義したい

愚直に定義してみる

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

contramapを使って定義する

  • contramapを使うともっと単純にeqToDoが得られる
  • contramapのシグニチャは以下
export declare const contramap: <A, B>(f: (b: B) => A) => (fa: Eq<A>) => Eq<B>
  • contramapは型クラスを勉強していると出てくるが、定義したいBEqを関数B => AAEqを与えることで、AEqに落とし込むことができる
  • 自前での実装はおそらく以下の感じ
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の書き換えは以下
import { contramap, eqNumber } from "fp-ts/lib/Eq"

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

実際にincludesを使ってみる

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

Point

// 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

// 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

// 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を使うなど、わざわざプリミティブな型に対してはそこまでするする必要もないとは思うが、複雑な構造をした型に対しては常に意識して使ったほうがメリットがある場合は積極的に使っていきたい

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?