April 29, 2021
본 포스트는 fp-ts 공식 문서의 Learning Resources에 있는 Getting Started에서 소개하는 문서들을 번역하며 학습한 문서입니다. 원본 문서는 링크에서 확인할 수 있으며 작성한 코드들은 여기에서 확인할 수 있습니다.
지난 포스트에서는 Semigroup
이 (concat
을 통해) 값 “병합”의 개념을 포착하는 것을 보았습니다. Monoid
는 concat
과 관련하여 “중립”인 특별한 값을 갖는 Semigroup
입니다.
fp-ts
의 fp-ts/lib/Monoid
모듈에 포함된 타입 클래스 Monoid
는 TypeScript interface
로 구현되며 empty
라고 이름이 지어진 중립 값이 존재합니다.
import { Semigroup } from 'fp-ts/lib/Semigroup';
interface Monoid<A> extends Semigroup<A> {
readonly empty: A;
}
Monoid
는 아래의 규칙이 유지되어야 합니다.
A
의 모든 x
에 대하여 concat(x, empty) = x
를 만족한다.A
의 모든 x
에 대하여 concat(empty, x) = x
를 만족한다.concat
의 어느 쪽이든 empty
값을 주어도 값에 차이가 없어야합니다.
참고:
empty
값이 있다면 고유합니다.
전에 살펴 본 대부분의 Semigroup
은 실제로 Monoid
입니다.
/** number 타입의 덧셈 `Monoid` */
export const monoidSum: Monoid<number> = {
concat: (x, y) => x + y,
empty: 0,
};
/** number 타입의 곱셈 `Monoid` */
export const monoidProduct: Monoid<number> = {
concat: (x, y) => x * y,
empty: 1,
};
export const monoidString: Monoid<string> = {
concat: (x, y) => x + y,
empty: '',
};
/** boolean타입의 논리곱 monoid */
export const monoidAll: Monoid<boolean> = {
concat: (x, y) => x && y,
empty: true,
};
/** boolean 타입의 논리합 `monoid` */
export const monoidAny: Monoid<boolean> = {
concat: (x, y) => x || y,
empty: false,
};
모든 Semigroup
이 Monoid
인지 궁금할 수 있지만 그렇지 않습니다. 반례로 아래의 Semigroup
을 고려할 수 있습니다.
const semigroupSpace: Semigroup<string> = {
concat: (x, y) => x + ' ' + y,
};
concat(x, empty) = x
를 만족하는 같은 empty
값을 찾을 수 없습니다.
더 복잡한 타입에 대한 Monoid
인스턴스를 작성해 보겠습니다. Point
와 같은 구조체에 대해 Monoid
인스턴스를 만들 수 있습니다.
type Point = {
x: number;
y: number;
};
구조체의 각 필드에 Monoid
인스턴스를 그대로 제공할 수 있습니다.
import { struct } from 'fp-ts/lib/Monoid';
type Point = {
x: number;
y: number;
};
const monoidPoint: Monoid<Point> = struct({
x: monoidSum,
y: monoidSum,
});
원문에서는
getStructMonoid
를 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며struct
를 사용하면 됩니다.
방금 정의된 인스턴스 또한 getStructMonoid
struct
를 사용해 계속 제공할 수 있습니다.
type Vector = {
from: Point;
to: Point;
};
const monoidVector: Monoid<Vector> = getStructMonoid({
from: monoidPoint,
to: monoidPoint,
});
Semigroup
대신 Monoid
를 사용하는 경우 folding이 더 간단합니다. 초깃 값을 명시적으로 제공할 필요가 없습니다. (구현에서는 Monoid
의 empty
값을 사용할 수 있습니다)
import { concatAll } from 'fp-ts/lib/Monoid';
concatAll(monoidSum)([1, 2, 3, 4]); // 10
concatAll(monoidProduct)([1, 2, 3, 4]); // 24
concatAll(monoidString)(['a', 'b', 'c']); // 'abc'
concatAll(monoidAll)([true, false, true]); // false
concatAll(monoidAny)([true, false, true]); // true
원문에서는
fold
를 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며concatAll
를 사용하면 됩니다.
A
에 대한 Semigroup
인스턴스가 주어지면 Option<A>
에 대한 Semigroup
인스턴스를 파생시킬 수 있다는 것을 이미 알고 있습니다.
A
에 대한 Monoid
인스턴스를 찾을 수 있다면 아래와 같이 작동하는 Option<A>
(getApplyMonoid
getApplicativeMonoid
를 통해)에 대한 Monoid
인스턴스를 파생시킬 수 있습니다.
원문에서는
getApplyMonoid
를 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며getApplicativeMonoid
,Applicative
를 사용하면 됩니다.
x | y | concat(x, y) |
---|---|---|
none | none | none |
some(a) | none | none |
none | some(a) | none |
some(a) | some(b) | some(concat(a, b)) |
import { Applicative } from 'fp-ts/lib/Option';
import { getApplicativeMonoid } from 'fp-ts/lib/Applicative';
export const appliedMonoidSum = getApplicativeMonoid(Applicative)(monoidSum);
작성한 appliedMonoidSum
인스턴스는 아래와 같이 테스트할 수 있습니다.
describe('Option타입을 지원하는 appliedMonoidSum 인스턴스 테스트', () => {
let result;
it('appliedMonoidSum 인스턴스 concat 함수 테스트 (some + none)', () => {
result = appliedMonoidSum.concat(some(1), none);
expect(result).toBe(none);
expect(isNone(result)).toBeTruthy();
});
it('appliedMonoidSum 인스턴스 concat 함수 테스트 (some + some)', () => {
result = appliedMonoidSum.concat(some(1), some(2));
expect(result).toMatchObject(some(3));
expect(isSome(result)).toBeTruthy();
});
it('appliedMonoidSum 인스턴스 concat 함수 테스트 (some + empty)', () => {
result = appliedMonoidSum.concat(some(1), appliedMonoidSum.empty);
expect(result).toMatchObject(some(1));
expect(isSome(result)).toBeTruthy();
});
});
구현된 appliedMonoidSum
인스턴스의 concat
함수가 표의 내용과 동일한 결과를 반환하는지 확인합니다.
Option<A>
에 대해 두 개의 다른 Monoid
를 파생시킬 수 있습니다.
getFirstMonoid
가장 왼쪽에 있는 None
이 아닌 값을 반환하는 Monoid
x | y | concat(x, y) |
---|---|---|
none | none | none |
some(a) | none | some(a) |
none | some(a) | some(a) |
some(a) | some(b) | some(a) |
import { getFirstMonoid } from 'fp-ts/lib/Option';
const firstMonoid = getFirstMonoid<number>();
작성된 firstMonoid
인스턴스는 아래와 같이 테스트할 수 있습니다.
describe('첫 번째 Some타입을 반환하는 firstMonoid 인스턴스 테스트', () => {
let result;
it('firstMonoid 인스턴스 concat 함수 테스트 (none + none)', () => {
result = firstMonoid.concat(none, none);
expect(result).toMatchObject(none);
expect(isNone(result)).toBeTruthy();
});
it('firstMonoid 인스턴스 concat 함수 테스트 (some + none)', () => {
result = firstMonoid.concat(some(1), none);
expect(result).toMatchObject(some(1));
expect(isSome(result)).toBeTruthy();
});
it('firstMonoid 인스턴스 concat 함수 테스트 (none + some)', () => {
result = firstMonoid.concat(none, some(1));
expect(result).toMatchObject(some(1));
expect(isSome(result)).toBeTruthy();
});
it('firstMonoid 인스턴스 concat 함수 테스트 (some + some)', () => {
result = firstMonoid.concat(some(1), some(2));
expect(result).toMatchObject(some(1));
expect(isSome(result)).toBeTruthy();
});
});
구현된 firstMonoid
인스턴스의 concat
함수가 표의 내용과 동일한 결과를 반환하는지 확인합니다.
getLastMonoid
가장 오른쪽에 있는 None
이 아닌 값을 반환하는 Monoid
x | y | concat(x, y) |
---|---|---|
none | none | none |
some(a) | none | some(a) |
none | some(a) | some(a) |
some(a) | some(b) | some(b) |
import { getLastMonoid } from 'fp-ts/lib/Option';
export const lastMonoid = getLastMonoid<number>();
작성된 lastMonoid
인스턴스는 아래와 같이 테스트할 수 있습니다.
describe('두 번째 Some타입을 반환하는 lastMonoid 인스턴스 테스트', () => {
let result;
it('lastMonoid 인스턴스 concat 함수 테스트 (none + none)', () => {
result = lastMonoid.concat(none, none);
expect(result).toMatchObject(none);
expect(isNone(result)).toBeTruthy();
});
it('lastMonoid 인스턴스 concat 함수 테스트 (some + none)', () => {
result = lastMonoid.concat(some(1), none);
expect(result).toMatchObject(some(1));
expect(isSome(result)).toBeTruthy();
});
it('lastMonoid 인스턴스 concat 함수 테스트 (none + some)', () => {
result = lastMonoid.concat(none, some(1));
expect(result).toMatchObject(some(1));
expect(isSome(result)).toBeTruthy();
});
it('lastMonoid 인스턴스 concat 함수 테스트 (some + some)', () => {
result = lastMonoid.concat(some(1), some(2));
expect(result).toMatchObject(some(2));
expect(isSome(result)).toBeTruthy();
});
});
구현된 lastMonoid
인스턴스의 concat
함수가 표의 내용과 동일한 결과를 반환하는지 확인합니다.
예를 들면 getLastMonoid
함수는 선택적인 값을 관리하는 데 유용할 수 있습니다.
원문에서는
getStructMonoid
를 사용하라고 작성되어 있지만, 최신 버전의 fp-ts에서는 deprecated 되어 있으며struct
를 사용하면 됩니다.
import type { Monoid } from 'fp-ts/lib/Monoid';
import type { Option } from 'fp-ts/lib/Option';
import { struct } from 'fp-ts/lib/Monoid';
import { getLastMonoid } from 'fp-ts/lib/Option';
/** VSCode 설정 */
export interface Settings {
/** font family를 제어한다. */
fontFamily: Option<string>;
/** font size 픽셀을 제어한다. */
fontSize: Option<number>;
/** 특정 수의 열만 렌더링하도록 미니 맵의 너비를 제한합니다. */
maxColumn: Option<number>;
}
export const monoidSettings: Monoid<Settings> = struct({
fontFamily: getLastMonoid<string>(),
fontSize: getLastMonoid<number>(),
maxColumn: getLastMonoid<number>(),
});
위와 같이 getLastMonoid
를 이용해 관리되는 속성들을 갖는 monoidSettings
인스턴스는 아래와 같이 테스트할 수 있습니다.
describe('getLastMonoid를 사용한 monoidSettings 인스턴스 테스트', () => {
const workspaceSettings: Settings = {
fontFamily: some('Courier'),
fontSize: none,
maxColumn: some(80),
};
const userSettings: Settings = {
fontFamily: some('Fira Code'),
fontSize: some(12),
maxColumn: none,
};
it('monoidSettings 인스턴스 concat 함수 테스트', () => {
const result = monoidSettings.concat(workspaceSettings, userSettings);
expect(isSome(result.fontFamily)).toBeTruthy();
expect(result.fontFamily).toMatchObject(some('Fira Code'));
expect(isSome(result.fontSize)).toBeTruthy();
expect(result.fontSize).toMatchObject(some(12));
expect(isSome(result.maxColumn)).toBeTruthy();
expect(result.maxColumn).toMatchObject(some(80));
});
});
monoidSettings
인스턴스의 concat
함수에 왼쪽에 기존 설정, 오른쪽에 사용자 설정을 넘겨 병합합니다. monoidSettings
인스턴스의 필드들은 getLastMonoid
로 만들어진 인스턴스로 이루어져 있기 때문에 오른쪽으로 전달된 userSettings
값 또는 None
이 아닌 값으로 결과가 병합되었는지 확인합니다.