April 25, 2021
본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Getting Started에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.
Eq에 대한 이전 블로그 게시물에서는 동등성에 대한 개념을 다루고 있었습니다. 이 블로그 게시물에서는 순서의 개념을 다루려고 합니다.
전체 순서를 허용하는 타입을 포함하는 타입 클래스 Ord
는 아래와 같은 방법으로 선언됩니다.
import { Eq } from 'fp-ts/lib/Eq';
type Ordering = -1 | 0 | 1;
interface Ord<A> extends Eq<A> {
readonly compare: (x: A, y: A) => Ordering;
}
두 개의 값 x
, y
는 아래와 같이 비교할 수 있습니다.
x < y
이거나 compare(x, y)
가 -1
인 경우x
와 y
가 같거나 compare(x, y)
가 0
인 경우x > y
이거나 compare(x, y)
가 1
인 경우결과적으로 compare(x, y) <= 0
인 경우에 x <= y
라고 말할 수 있습니다.
아래 예시는 number
타입에 대한 Ord
인스턴스입니다.
const ordNumber: Ord<number> = {
equals: (x, y) => x === y,
compare: (x, y) => (x < y ? -1 : x > y ? 1 : 0),
};
인스턴스는 아래의 규칙을 만족합니다.
A
의 모든 x
에 대하여 compare(x, x) === 0
을 만족한다.A
의 모든 x
, y
에 대하여 compare(x, y) <= 0
이고 compare(y, x) <= 0
이면 x
와 y
는 같다.A
의 모든 x
, y
, z
에 대하여 compare(x, y) <= 0
이고 compare(y, z) <= 0
이면 compare(x, z) <= 0
을 만족한다.또한 compare
은 Eq
의 equals
와 일치해야 합니다.
A
의 모든 x
, y
에 대하여 compare(x, y) === 0
일 경우 equals(x, y) === true
의 경우만 만족합니다.
참고: 규칙에 따르는
equals
는 아래와 같이compare
에서 도출할 수 있습니다.
equals: (x, y) => compare(x, y) === 0;
실제로 fp-ts/lib/Ord
모듈에는 간단히 비교 함수를 지정하여 Ord
인스턴스를 정의할 수 있는 편리한 fromCompare
헬퍼 함수가 있습니다.
import type { Ord } from 'fp-ts/lib/Ord';
import { fromCompare } from 'fp-ts/lib/Ord';
const ordNumber: Ord<number> = fromCompare((x, y) =>
x < y ? -1 : x > y ? 1 : 0
);
프로그래머는 아래와 같은 방법으로 min
함수를 정의할 수 있습니다.
function min<A>(O: Ord<A>): (x: A, y: A) => A {
return (x, y) => (O.compare(x, y) === 1 ? y : x);
}
작성한 min
함수는 아래와 같이 테스트할 수 있습니다. min
함수는 Ord
인스턴스를 받아 x
, y
의 값을 compare
로 함수로 비교해 x
, y
중에서 작은 값을 반환합니다.
describe('Ord 인터페이스를 이용한 min 함수 테스트', () => {
it('ordNumber를 이용한 min 함수 테스트 ', () => {
expect(min(ordNumber)(2, 1)).toBe(1);
expect(min(ordNumber)(2, 3)).toBe(2);
});
});
toBe
함수를 이용해 min
함수가 반환한 함수에 인자로 넣은 값 중에 작은 값이 정상적으로 반환되는지 확인할 수 있습니다.
number
타입에 관해 이야기 할 때에는 전체성(Totality)은 분명해 보일 수 있지만 (즉, x <= y
또는 y <= x
) 항상 그런 것은 아닙니다. 더 복잡한 타입을 고려해 볼 수 있습니다.
type User = {
name: string;
age: number;
};
Ord<User>
인터페이스를 어떻게 정의할 수 있나요?
실제로는 상황에 따라 다르지만 가능한 선택으로는 age
속성으로 사용자를 정렬하는 것입니다.
const byAge: Ord<User> = fromCompare((x, y) => ordNumber.compare(x.age, y.age));
contramap
콤비네이터를 사용하여 일부 자주 사용하는 구문을 피할 수 있습니다. A
에 대한 Ord
인스턴스와 B
에서 A
로의 함수가 주어지면 B
에 대한 Ord
의 인스턴스를 파생시킬 수 있습니다.
import { contramap } from 'fp-ts/lib/Ord';
const byAge: Ord<User> = contramap((user: User) => user.age)(ordNumber);
이제 min
을 사용하여 두 User
중 더 어린 User
를 선택할 수 있습니다.
const getYounger = min(byAge);
작성한 getYounger
함수는 아래와 같이 테스트할 수 있습니다.
describe('byAge 함수와 min 함수를 사용하는 getYounger 함수 테스트', () => {
it('getYounger 함수 테스트 ', () => {
expect(
getYounger({ name: 'Guido', age: 48 }, { name: 'Giulio', age: 45 })
).toMatchObject({ name: 'Giulio', age: 45 });
});
});
getYounger
함수로 전달된 두 User
타입 객체 중 age
속성의 값이 작은 User
타입 객체가 정상적으로 반환되는지 확인할 수 있습니다.
반대로 더 오래된 것을 선택하려면 어떻게 할 수 있을까요? 우리는 “순서를 뒤집거나” 기술적으로 말하면 이중 정렬을 받아야 합니다.
다행히도 이런 상황을 위해 fp-ts
가 제공하는 다른 콤비네이터가 있습니다.
원문에서는
getDualOrd
를 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며reverse
를 사용하면 됩니다.
import type { Ord } from 'fp-ts/lib/Ord';
import { reverse } from 'fp-ts/lib/Ord';
import { byAge } from './byAge';
import { min } from './min';
function max<A>(O: Ord<A>): (x: A, y: A) => A {
return min(reverse(O));
}
const getOlder = max(byAge);
작성한 getOlder
함수는 아래와 같이 테스트할 수 있습니다.
describe('byAge, min, reverse 함수를 사용하는 getOlder 함수 테스트', () => {
it('getOlder 함수 테스트 ', () => {
expect(
getOlder({ name: 'Guido', age: 48 }, { name: 'Giulio', age: 45 })
).toMatchObject({ name: 'Guido', age: 48 });
});
});
getOlder
함수로 전달된 두 User
타입 객체 중 age
속성의 값이 큰 User
타입 객체가 정상적으로 반환되는지 확인할 수 있습니다.