fp-ts を使ってみながら関数型指向の理解を進める(Eq 編)
Eq とは
ライブラリのドキュメントから、Eq のインターフェースは以下の通り。型A
をジェネリクスで受け取り、equals
プロパティを持つオブジェクトである。
equal
プロパティはジェネリクスで受け取った型A
の引数x
、y
を受け取り boolean を返す関数であることが分かる。
export interface Eq<A> {
readonly equals: (x: A, y: A) => boolean;
}
Eq は、集合 A 内の要素に対して等価であることはどういうことかを定義するものである。そして、この等価性をequals
メソッドを実行することにより判定する。すなわち以下を満たす関数$f(x,y)$をequals
プロパティに設定するのである。
\forall x, \forall y \in A \\
f(x, y) = \left\{
\begin{array}{ll}
\mathrm{true} & (x = y) \\
\mathrm{false} & (x \neq y)
\end{array}
\right.
誤解を恐れずに言えば集合A
は TypeScript の型に相当すると考えて問題ない。
Eq のインターフェースが分かったところで、具体的な集合A
を考えて Eq のインターフェースを実装に使ってみる。
これ以降のコードで使っている Eq モジュールは以下のようにインポートされてあることを前提としている。
import { fromEquals, struct, Eq, tuple, contramap } from 'fp-ts/lib/Eq';
A = number
のとき
const eqNumber: Eq<number> = {
equals: (x, y) => {
return x === y
};
};
そして、この Eq は以下のように等価性の判定に使用できる。
expect(eqNumber.equals(1, 1)).toBe(true);
expect(eqNumber.equals(1, 2)).toBe(false);
実はよく使う型に対しては標準の Eq が用意されてあるので、今回実装した eqNumber
は用意されてあるものを使えば良い。他にも string 型や boolean 型に対してデフォルトの Eq が用意されてある。
import * as N from 'fp-ts/number';
import * as S from 'fp-ts/string';
import * as B from 'fp-ts/boolean';
expect(N.Eq.equals(1, 1)).toBe(true);
expect(N.Eq.equals(1, 2)).toBe(false);
expect(S.Eq.equals('a', 'a')).toBe(true);
expect(S.Eq.equals('a', 'b')).toBe(false);
expect(B.Eq.equals(true, true)).toBe(true);
expect(B.Eq.equals(true, false)).toBe(false);
A = Point
のとき
次にequals
関数が===
ではない場合を考えてみよう
ここで 2 次元座標平面上の点を表すPoint
型をについて、eqPoint
を実装してみることを考えてみよう
ここで考えるPoint
の型は以下の通り
export type Point = {
x: number;
y: number;
};
Point
型の変数p1
、p2
が等しいとは、x
、y
フィールドのそれぞれの値が等しいことと考えると、eqPoint
は以下のように実装できる。(ジェネリクスでPoint
型を渡すことでequals
メソッドの引数p1
、p2
に対して型推論が効く)
const eqPoint: Eq<Point> = {
equals: (p1, p2) => {
return p1.x === p2.x && p1.y === p2.y;
},
};
実装したeqPoint
の使い方は以下の通り
const p1: Point = { x: 1, y: 2 };
const p2: Point = { x: 1, y: 2 };
const p3: Point = { x: 0, y: 0 };
expect(eqPoint.equals(p1, p2)).toBe(true);
expect(eqPoint.equals(p1, p3)).toBe(false);
Eq モジュールに含まれるその他の使い方
fromEquals
fromEquals
はequals
関数から Eq を作る関数
const equal = (p1: Point, p2: Point) => {
return p1.x === p2.x && p1.y === p2.y;
};
const eqPoint: Eq<Point> = fromEquals(equal);
expect(eqPoint.equals(p1, p2)).toBe(true);
expect(eqPoint.equals(p1, p3)).toBe(false);
contramap
contramap
の型シグネチャは以下の通り
export declare const contramap: <A, B>(f: (b: B) => A) => (fa: Eq<A>) => Eq<B>;
集合$A,B$に対して、$B$から$A$に対しての写像$f:B \rightarrow A$と、$A$に対しての等価判定eq<A>
を入力とし、$B$に対しての等価判定Eq<B>
を作る関数
ドキュメントにある例であるが、以下ようなUser
型の一致判定をid
プロパティの比較で行うEq<User>
を作りたい場合を考える。
// 分かりやすいようにあえてId型を定義した。
type Id = string;
type User = {
id: Id;
name: string;
age: number;
};
以下のように、今、手元に持っているのはId
型に対する等価を表すeqId:Eq<Id>
とUser
型からId
型への変換関数userToIdFunc
であるとする。
先程の集合$A$がId
型に、集合$B$がUser
型に、写像$f:B \rightarrow A$がuserToIdFunc
関数に対応している。
const eqId: Eq<Id> = S.Eq;
const userToIdFunc: (user: User) => Id = (user) => user.id;
eqId
とuserToIdFunc
を使ってEq<User>
を以下のように作ることができる。
const eqUser: Eq<User> = contramap(userToIdFunc)(eqId);
また、pipe
を使えば以下のようにも書ける。pipe
についてはfunction
モジュールの記事で説明しますが、比較すれば、pipe
の使い方は理解してもらえるかと思う。
const eqUserWithPipe: Eq<User> = pipe(eqId, contramap(userToIdFunc));
Eq<User>
も他の Eq と同様に使うことができる。
import { pipe } from 'fp-ts/function';
const user1: User = { id: 'b', name: 'Bob', age: 20 };
const user2: User = { id: 'b', name: 'Bobby', age: 20 };
const user3: User = { id: 'a', name: 'Alice', age: 23 };
expect(eqUser.equals(user1, user2)).toBe(true);
expect(eqUser.equals(user1, user3)).toBe(false);
expect(eqUserWithPipe.equals(user1, user2)).toBe(true);
expect(eqUserWithPipe.equals(user1, user3)).toBe(false);
struct
オブジェクトのプロパティそれぞれに対して Eq を作り、そこから、オブジェクト全体に対しての Eq を作成する関数。
// プロパティそれぞれのEq
const eqNumber = (n1: number, n2: number) => n1 === n2;
// オブジェクト全体のEq
// 各プロパティにプロパティそれぞれのEqそ設定してやる
const eqPoint = struct({
x: fromEquals(eqNumber),
y: fromEquals(eqNumber),
});
expect(eqPoint.equals(p1, p2)).toBe(true);
expect(eqPoint.equals(p1, p3)).toBe(false);
tuple
struct
の配列バージョン。配列のそれぞれのインデックスに対して、Eq を作り、そこから、配列全体に対しての Eq を作成する関数。
const eqTuple = tuple(N.Eq, S.Eq, B.Eq);
expect(eqTuple.equals([1, '1', true], [1, '1', true])).toBe(true);
expect(eqTuple.equals([1, '1', true], [2, '1', true])).toBe(false);