1
0

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 1 year has passed since last update.

fp-ts を使ってみながら関数型指向の理解を進める(Eq 編)

Last updated at Posted at 2023-05-07

fp-ts を使ってみながら関数型指向の理解を進める(Eq 編)

Eq とは

ライブラリのドキュメントから、Eq のインターフェースは以下の通り。型Aをジェネリクスで受け取り、equalsプロパティを持つオブジェクトである。
equalプロパティはジェネリクスで受け取った型Aの引数xyを受け取り 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型の変数p1p2が等しいとは、xyフィールドのそれぞれの値が等しいことと考えると、eqPointは以下のように実装できる。(ジェネリクスでPoint型を渡すことでequalsメソッドの引数p1p2に対して型推論が効く)

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

fromEqualsequals関数から 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;

eqIduserToIdFuncを使って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);
1
0
1

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?