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
함수를 이용해 값이 같은지 확인합니다.