April 24, 2021
본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Getting Started에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.
이 블로그 시리즈에서는 종종 “타입 클래스”와 “인스턴스”에 대해 이야기할 것입니다. 그것들이 무엇이고 fp-ts에서 어떻게 인코딩되는지 살펴보겠습니다.
프로그래머는 클래스에 속하는 모든 유형에 대해 존재해야 하는 해당 타입과 함께 함수 또는 상수 이름 집합을 지정하여 타입 클래스를 정의합니다.
fp-ts에서 타입 클래스는 TypeScript의 interface로 인코딩됩니다.
동등성을 허용하는 타입을 포함하기 위한 타입 클래스 Eq는 아래와 같이 선언됩니다.
interface Eq<A> {
/** `x`와 `y`가 같을 경우 `true`를 반환한다. */
readonly equals: (x: A, y: A) => boolean;
}선언은 아래와 같이 읽을 수 있습니다.
A타입은 정의된 적절한 타입과equal이라는 이름의 함수가 있는 경우 타입 클래스Eq에 속합니다.
instance는 아래와 같습니다.
프로그래머는 특정 타입
A에 대한 모든C멤버의 구현을 정의하는 인스턴스 선언을 사용하여 어떤 타입A를 주어진 타입 클래스C의 멤버로 만들 수 있습니다.
fp-ts 인스턴스는 정적인 딕셔너리로 인코딩됩니다.
예를 들어 아래는 number 타입에 대한 Eq의 인스턴스입니다.
const eqNumber: Eq<number> = {
equals: (x, y) => x === y,
};인스턴스는 아래의 규칙을 만족합니다.
A의 모든 x에 대하여 equals(x, x) === true를 만족한다.A의 모든 x, y에 대하여 equals(x, y) === equals(y, x)를 만족한다.A의 모든 x, y, z에 대하여 equals(x, y) === true이고 equals(y, z) === true라면 equals(x, z) === true를 만족한다.elem 함수프로그래머는 아래와 같은 방법으로 elem(요소가 배열에 있는지를 결정하는) 함수를 정의할 수 있다.
function elem<A>(E: Eq<A>): (a: A, as: Array<A>) => boolean {
return (a, as) => as.some(item => E.equals(item, a));
}number타입의 Eq 인스턴스인 eqNumber를 이용한 elem 함수를 테스트하는 코드는 아래와 같다. elem 함수가 반환하는 boolean 타입값을 이용해 toBeTruthy, toBeFalsy 함수를 이용해 확인할 수 있다.
describe('elem 함수 테스트', () => {
it('eqNumber를 이용한 elem 함수 테스트 (요소가 있는 경우)', () => {
expect(elem(eqNumber)(1, [1, 2, 3])).toBeTruthy();
});
it('eqNumber를 이용한 elem 함수 테스트 (요소가 없는 경우)', () => {
expect(elem(eqNumber)(4, [1, 2, 3])).toBeFalsy();
});
});더 복잡한 타입에 대한 Eq 인스턴스를 작성해 보겠습니다.
type Point = {
x: number;
y: number;
};
const eqPoint: Eq<Point> = {
equals: (p1, p2) => p1.x === p2.x && p1.y === p2.y,
};참조 동등성을 먼저 확인하여 equals를 최적화 할 수도 있습니다.
const eqPoint: Eq<Point> = {
equals: (p1, p2) => p1 === p2 || (p1.x === p2.x && p1.y === p2.y),
};하지만 이것은 대부분 보일러플레이트 입니다. 좋은 소식은 우리가 각 필드에 Eq 인스턴스를 제공할 수 있다면 Point와 같은 구조에 대한 Eq 인스턴스를 만들 수 있다는 것이다.
실제로 fp-ts/lib/Eq 모듈이 getStructEq 콤비네이터을 내보냅니다.
원문에서는
getStructEq를 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며struct를 사용하면 됩니다.
const eqPoint: Eq<Point> = struct({
x: eqNumber,
y: eqNumber,
});방금 정의한 인스턴스로 struct를 계속 지원할 수 있습니다.
const eqVector: Eq<Vector> = struct({
from: eqPoint,
to: eqPoint,
});struct는 fp-ts에 의해 제공되는 유일한 콤비네이터가 아닙니다. fp-ts/lib/Array에는 배열을 위한 Eq 인스턴스를 도출할 수 있는 콤비네이터가 있습니다.
import { getEq } from 'fp-ts/lib/Array';
const eqArrayOfPoints: Eq<Array<Point>> = getEq(eqPoint);마지막으로 Eq 인스턴스를 구축할 수 있는 또 다른 유용한 방법은 contramap 콤비네이터입니다.
contramap 콤비네이터 타입 정의const contramap = <A, B>(f: (b: B) => A) => (fa: Eq<A>) => Eq<B>;contramap 콤비네이터는 A에 대한 Eq의 인스턴스와 B에서 A로의 함수가 주어지면 B에 대한 Eq의 인스턴스를 파생시킬 수 있습니다.
import { contramap } from 'fp-ts/lib/Eq';
import { eqNumber } from './eqNumber';
type User = {
userId: number;
name: string;
};
/** 두 User는`userId` 필드가 같으면 같습니다. */
export const eqUser = contramap((user: User) => user.userId)(eqNumber);eqUser에서 사용한 contramap 콤비네이터는 A 타입으로 number 타입을 받고 B 타입으로 User 타입을 받으며 number 타입인 User 타입의 userId 필드를 eqNumber 함수를 이용해 동등성을 판단합니다.
eqUser에 사용된 contramp 함수의 타입 정의const contramap = <number, User>(f: (b: User) => number) =>
(fa: Eq<number>) => Eq<User>;작성한 eqUser 인스턴스는 아래와 같이 테스트할 수 있습니다.
describe('Eq 인터페이스를 구현한 eqUser 인스턴스 테스트', () => {
it('eqUser 인스턴스 equals 함수 테스트', () => {
expect(
eqUser.equals(
{ userId: 1, name: 'Giulio' },
{ userId: 1, name: 'Giulio Canti' }
)
).toBeTruthy();
expect(
eqUser.equals(
{ userId: 1, name: 'Giulio' },
{ userId: 2, name: 'Giulio' }
)
).toBeFalsy();
});
});eqUser 인스턴스의 equals 함수는 두 개의 인자로 받은 두 User 타입 객체의 userId가 같은 경우 true를 반환합니다. userId 필드는 앞에서 구현했던 eqNumber 인스턴스의 equals 함수를 이용해 값이 같은지 확인합니다.