fp-ts둜 Typescript ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ° μ‹œμž‘ν•˜κΈ° 3 (Semigroup)

λ³Έ ν¬μŠ€νŠΈλŠ” fp-ts 곡식 λ¬Έμ„œμ˜ Learning Resources에 μžˆλŠ” Getting Startedμ—μ„œ μ†Œκ°œν•˜λŠ” λ¬Έμ„œλ“€μ„ λ²ˆμ—­ν•˜λ©° ν•™μŠ΅ν•œ λ¬Έμ„œμž…λ‹ˆλ‹€. 원본 λ¬Έμ„œλŠ” λ§ν¬μ—μ„œ 확인할 수 있으며 μž‘μ„±ν•œ μ½”λ“œλ“€μ€ μ—¬κΈ°μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

fp-ts μ‹œμž‘ν•˜κΈ° (Semigroup)

Semigroup은 ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ˜ 근본적인 μΆ”μƒν™”μ΄λ―€λ‘œ 이 κΈ€μ˜ λ‚΄μš©μ΄ ν‰μ†Œλ³΄λ‹€ κΈΈμ–΄μ§ˆ κ²ƒμž…λ‹ˆλ‹€.

일반적인 μ •μ˜

Semigroup은 Aκ°€ λΉ„μ–΄ μžˆμ§€ μ•Šμ€ 집합이고 *κ°€ A에 λŒ€ν•œ 이진 μ—°κ΄€ 연산인 쌍 (A, *)μž…λ‹ˆλ‹€. 즉, A의 두 μš”μ†Œλ₯Ό μž…λ ₯으둜 λ°›κ³  A의 μš”μ†Œλ₯Ό 좜λ ₯으둜 λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜μž…λ‹ˆλ‹€.

*: (x: A, Y: A) => A

κ²°ν•© 법칙은 μ•„λž˜μ˜ 동식이 λͺ¨λ“  A에 λŒ€ν•œ x, y, z에 λŒ€ν•΄ μœ μ§€λ¨μ„ μ˜λ―Έν•œλ‹€.

(x * y) * z = x * (y * z)

κ²°ν•© 법칙은 λ‹¨μˆœνžˆ ν‘œν˜„μ‹μ„ κ΄„ν˜Έλ‘œ λ¬ΆλŠ” 것에 λŒ€ν•΄ κ±±μ •ν•  ν•„μš”κ°€ μ—†μœΌλ©° x * y * zλ₯Ό μ“Έ 수 μžˆλ‹€λŠ” 것을 μ˜λ―Έν•©λ‹ˆλ‹€.

Semigroup은 병렬화 κ°€λŠ₯ν•œ μ—°μ‚°μ˜ λ³Έμ§ˆμ„ ν¬μ°©ν•©λ‹ˆλ‹€.

Semigroup의 μ˜ˆμ‹œλŠ” μ•„λž˜μ™€ 같이 많이 μžˆμŠ΅λ‹ˆλ‹€.

  • (number, *): μ—¬κΈ°μ—μ„œ * 연산은 일반적인 숫자의 κ³±μž…λ‹ˆλ‹€.
  • (string, +): μ—¬κΈ°μ—μ„œ + 연산은 일반적인 λ¬Έμžμ—΄ μ—°κ²°μž…λ‹ˆλ‹€.
  • (boolean, &&): μ—¬κΈ°μ—μ„œ && 연산은 일반적인 λ…Όλ¦¬κ³±μž…λ‹ˆλ‹€.

이 외에도 λ§Žμ€ μ˜ˆμ‹œκ°€ μžˆμŠ΅λ‹ˆλ‹€.

νƒ€μž… 클래슀 μ •μ˜

fp-tsμ—μ„œ fp-ts/lib/Semigroupλͺ¨λ“ˆμ— ν¬ν•¨λœ νƒ€μž… 클래슀 Semigroup은 TypeScript의 interface둜 κ΅¬ν˜„λ©λ‹ˆλ‹€. μ—¬κΈ°μ„œ μž‘μ—… *λŠ” concat으둜 λͺ…λͺ…λ©λ‹ˆλ‹€.

interface Semigroup<A> {
  concat: (x: A, y: A) => A;
}

Semigroup은 μ•„λž˜μ˜ κ·œμΉ™μ΄ μœ μ§€λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€.

  1. κ²°ν•© 법칙(Associativity): A의 λͺ¨λ“  x, y, z에 λŒ€ν•˜μ—¬ concat(concat(x, y), z) = concat(x, concat(y, z))λ₯Ό λ§Œμ‘±ν•œλ‹€.

concatμ΄λΌλŠ” 이름은 배열에 λŒ€ν•΄ 특히 μ˜λ―Έκ°€ μžˆμ§€λ§Œ, μΈμŠ€ν„΄μŠ€λ₯Ό κ΅¬ν˜„ν•˜λŠ” λ§₯락 및 νƒ€μž… A에 따라 Semigroup 연산은 λ‹€λ₯Έ 의미둜 해석될 수 μžˆμŠ΅λ‹ˆλ‹€.

  • 연쇄(concatenation)
  • 병합(merging)
  • 퓨전(fusion)
  • 선택(selection)
  • λΆ€κ°€(addition)
  • μΉ˜ν™˜(substitution)

이 외에도 λ‹€λ₯Έ λ§Žμ€ 의미둜 해석될 수 μžˆμŠ΅λ‹ˆλ‹€.

μΈμŠ€ν„΄μŠ€

μ•„λž˜μ˜ semigroupProduct μΈμŠ€ν„΄μŠ€κ°€ (number, *)을 κ΅¬ν˜„ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€.

/** number νƒ€μž…μ˜ κ³±μ…ˆ `Semigroup` */
export const semigroupProduct: Semigroup<number> = {
  concat: (x, y) => x * y,
};

λ™μΌν•œ νƒ€μž…μ— λŒ€ν•΄ μ„œλ‘œ λ‹€λ₯Έ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό μ •μ˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ•„λž˜λŠ” semigroupProductSum μΈμŠ€ν„΄μŠ€λ‘œ (number, +)의 κ΅¬ν˜„μž…λ‹ˆλ‹€. μ—¬κΈ°μ„œ +λŠ” 일반적인 number νƒ€μž…μ˜ λ”ν•˜κΈ° μ—°μ‚°μž…λ‹ˆλ‹€.

/** number νƒ€μž…μ˜ λ§μ…ˆ `Semigroup` */
const semigroupSum: Semigroup<number> = {
  concat: (x, y) => x + y,
};

λ‹€λ₯Έ μ˜ˆμ‹œλ‘œ string νƒ€μž…μ„ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

const semigroupString: Semigroup<string> = {
  concat: (x, y) => x + y,
};

μΈμŠ€ν„΄μŠ€λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€!

νƒ€μž… Aκ°€ μ£Όμ–΄μ‘Œμ„ λ•Œ Aμ—μ„œ μ—°κ΄€ 연산을 찾을 수 μ—†μœΌλ©΄ μ–΄λ–»κ²Œ ν•  수 μžˆμ„κΉŒμš”? μ•„λž˜μ˜ ꡬ성을 μ‚¬μš©ν•˜μ—¬ λͺ¨λ“  νƒ€μž…μ— λŒ€ν•΄ (μ‚¬μ†Œν•œ) Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€.

/** 항상 첫 번째 인자λ₯Ό λ°˜ν™˜ν•œλ‹€. */
function getFirstSemigroup<A = never>(): Semigroup<A> {
  return { concat: (x, y) => x };
}

/** 항상 두 번째 인자λ₯Ό λ°˜ν™˜ν•œλ‹€. */
function getLastSemigroup<A = never>(): Semigroup<A> {
  return { concat: (x, y) => y };
}

또 λ‹€λ₯Έ κΈ°μˆ μ€ A의 자유 Semigroupμ΄λΌκ³ ν•˜λŠ” Array<A> (*)에 λŒ€ν•œ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό μ •μ˜ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€.

function getArraySemigroup<A = never>(): Semigroup<Array<A>> {
  return { concat: (x, y) => x.concat(y) };
}

그리고 A의 μš”μ†Œλ₯Ό Array<A>의 단일 μš”μ†Œμ— λ§€ν•‘ν•©λ‹ˆλ‹€.

function of<A>(a: A): Array<A> {
  return [a];
}

(*)λŠ” μ—„λ°€νžˆ λ§ν•˜λ©΄ A의 λΉ„μ–΄ μžˆμ§€ μ•Šμ€ 배열에 λŒ€ν•œ Semigroup μΈμŠ€ν„΄μŠ€μž…λ‹ˆλ‹€.

μ°Έκ³ : concat은 λ°°μ—΄μ˜ λ©”μ„œλ“œλ‘œ, Semigroup μ—°μ‚°μ˜ 이름에 λŒ€ν•œ 초기 선택을 μ„€λͺ…ν•©λ‹ˆλ‹€.

A의 자유 Semigroup은 μš”μ†Œκ°€ A μš”μ†Œμ˜ λΉ„μ–΄μžˆμ§€ μ•Šμ€ μœ ν•œ μ‹œν€€μŠ€μΌ 수 μžˆλŠ” Semigroupμž…λ‹ˆλ‹€.

Ord둜 νŒŒμƒμ‹œν‚€κΈ°

νƒ€μž… A에 λŒ€ν•œ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό λ§Œλ“œλŠ” 또 λ‹€λ₯Έ 방법이 μžˆμŠ΅λ‹ˆλ‹€. A에 λŒ€ν•œ Ord μΈμŠ€ν„΄μŠ€κ°€ μ΄λ―ΈμžˆλŠ” 경우 이λ₯Ό Semigroup으둜 β€œλ³€ν™˜β€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

μ•„λž˜ μ½”λ“œλŠ” μ‹€μ œλ‘œ κ°€λŠ₯ν•œ 두 Semigroupμž…λ‹ˆλ‹€.

μ›λ¬Έμ—μ„œλŠ” getMeetSemigroup, getJoinSemigroup, ordNumberλ₯Ό μ‚¬μš©ν•˜λΌκ³  μž‘μ„±λ˜μ–΄ μžˆμ§€λ§Œ, μ΅œμ‹  λ²„μ „μ˜ fp-tsμ—μ„œλŠ” deprecated λ˜μ–΄ 있으며 min, max, Ordλ₯Ό μ‚¬μš©ν•˜λ©΄ λ©λ‹ˆλ‹€.

import { Ord } from 'fp-ts/lib/number';
import { max, min } from 'fp-ts/lib/Semigroup';

/** 2개의 κ°’ 쀑 μž‘μ€ 값을 λ°˜ν™˜ν•œλ‹€.  */
const semigroupMin: Semigroup<number> = min(Ord);

/** 2개의 κ°’ 쀑 큰 값을 λ°˜ν™˜ν•œλ‹€.  */
const semigroupMax: Semigroup<number> = max(Ord);

μž‘μ„±ν•œ semigroupMin, semigroupMax μΈν„°νŽ˜μ΄μŠ€λŠ” μ•„λž˜μ™€ 같이 ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.

  • semigroupMin μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν…ŒμŠ€νŠΈν•˜λŠ” μ½”λ“œ
describe('Semigroup μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•œ semigroupMin μΈμŠ€ν„΄μŠ€ ν…ŒμŠ€νŠΈ', () => {
  it('semigroupMin μΈμŠ€ν„΄μŠ€ concat ν•¨μˆ˜ ν…ŒμŠ€νŠΈ', () => {
    expect(semigroupMin.concat(2, 1)).toBe(1);
  });
});
  • semigroupMax μΈν„°νŽ˜μ΄μŠ€λ₯Ό ν…ŒμŠ€νŠΈν•˜λŠ” μ½”λ“œ
describe('Semigroup μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•œ semigroupMax μΈμŠ€ν„΄μŠ€ ν…ŒμŠ€νŠΈ', () => {
  it('semigroupMax μΈμŠ€ν„΄μŠ€ concat ν•¨μˆ˜ ν…ŒμŠ€νŠΈ', () => {
    expect(semigroupMax.concat(2, 1)).toBe(2);
  });
});

semigroupMin μΈν„°νŽ˜μ΄μŠ€μ˜ concat ν•¨μˆ˜λŠ” 인자둜 받은 두 개의 κ°’ 쀑 μž‘μ€ 값을 λ°˜ν™˜ν•˜λŠ”μ§€ ν™•μΈν•˜λ©° semigroupMax μΈν„°νŽ˜μ΄μ‹€ concat ν•¨μˆ˜λŠ” 인자둜 받은 두 개의 κ°’ 쀑 큰 값을 λ°˜ν™˜ν•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.

μ’€ 더 λ³΅μž‘ν•œ νƒ€μž…μ— λŒ€ν•œ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό μž‘μ„±ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

type Point = {
  x: number;
  y: number;
};

const semigroupPoint: Semigroup<Point> = {
  concat: (p1, p2) => ({
    x: semigroupSum.concat(p1.x, p2.x),
    y: semigroupSum.concat(p1.y, p2.y),
  }),
};

μœ„ μ½”λ“œμ˜ λŒ€λΆ€λΆ„ 자주 μ‚¬μš©ν•˜λŠ” κ΅¬λ¬Έμž…λ‹ˆλ‹€. 쒋은 μ†Œμ‹μ€ 각 ν•„λ“œμ— λŒ€ν•΄ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό 제곡 ν•  수 μžˆλ‹€λ©΄ Point와 같은 ꡬ쑰체에 λŒ€ν•΄ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό λ§Œλ“€ 수 μžˆλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€.

μ‹€μ œλ‘œ fp-ts/lib/Semigroup λͺ¨λ“ˆμ€ getStructSemigroup 콀비넀이터λ₯Ό μ§€μ›ν•©λ‹ˆλ‹€.

μ›λ¬Έμ—μ„œλŠ” getStructSemigroupλ₯Ό μ‚¬μš©ν•˜λΌκ³  μž‘μ„±λ˜μ–΄ μžˆμ§€λ§Œ, μ΅œμ‹  λ²„μ „μ˜ fp-tsμ—μ„œλŠ” deprecated λ˜μ–΄ 있으며 structλ₯Ό μ‚¬μš©ν•˜λ©΄ λ©λ‹ˆλ‹€.

import { struct } from 'fp-ts/lib/Semigroup';

const semigroupPoint: Semigroup<Point> = struct({
  x: semigroupSum,
  y: semigroupSum,
});

κ³„μ†ν•΄μ„œ 방금 μ •μ˜ 된 μΈμŠ€ν„΄μŠ€λ‘œ structλ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

type Vector = {
  from: Point;
  to: Point;
};

const semigroupVector: Semigroup<Vector> = struct({
  from: semigroupPoint,
  to: semigroupPoint,
});

struct은 fp-tsμ—μ„œ μ œκ³΅ν•˜λŠ” μœ μΌν•œ 콀비넀이터가 μ•„λ‹™λ‹ˆλ‹€. 여기에 ν•¨μˆ˜μ— λŒ€ν•œ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό νŒŒμƒμ‹œν‚¬ 수 μžˆλŠ” 콀비넀이터가 μžˆμŠ΅λ‹ˆλ‹€. S에 λŒ€ν•œ Semigroup μΈμŠ€ν„΄μŠ€κ°€ 주어지면, λͺ¨λ“  A에 λŒ€ν•΄ μ‹œκ·Έλ‹ˆμ²˜ (a: A) => S에 ν•΄λ‹Ήν•˜λŠ” Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό λ„μΆœν•  수 μžˆλ‹€.

μ›λ¬Έμ—μ„œλŠ” getFunctionSemigroup, semigroupAllλ₯Ό μ‚¬μš©ν•˜λΌκ³  μž‘μ„±λ˜μ–΄ μžˆμ§€λ§Œ, μ΅œμ‹  λ²„μ „μ˜ fp-tsμ—μ„œλŠ” deprecated λ˜μ–΄ 있으며 getSemigroup, SemigroupAllλ₯Ό μ‚¬μš©ν•˜λ©΄ λ©λ‹ˆλ‹€.

import type { Semigroup } from 'fp-ts/lib/Semigroup';
import type { Point } from './semigroupPoint';
import { getSemigroup } from 'fp-ts/lib/function';
import { SemigroupAll } from 'fp-ts/lib/boolean';

/** `semigroupAll`은 κ²°ν•© 된 boolean Semigroupμž…λ‹ˆλ‹€. */
export const semigroupPredicate: Semigroup<
  (p: Point) => boolean
> = getSemigroup(SemigroupAll)<Point>();

이제 Pointsμ—μ„œ 두 predicate ν•¨μˆ˜λ₯Ό β€œλ³‘ν•©β€ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

const isPositiveX = (p: Point): boolean => p.x >= 0;
const isPositiveY = (p: Point): boolean => p.y >= 0;

const isPositiveXY = semigroupPredicate.concat(isPositiveX, isPositiveY);

μž‘μ„±ν•œ isPositiveXY ν•¨μˆ˜λŠ” μ•„λž˜μ™€ 같이 ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.

describe('semigroupPredicate μΈμŠ€ν„΄μŠ€λ₯Ό μ΄μš©ν•΄ λ§Œλ“  isPositiveXY ν…ŒμŠ€νŠΈ', () => {
  it('isPositiveXY ν•¨μˆ˜ ν…ŒμŠ€νŠΈ', () => {
    expect(isPositiveXY({ x: 1, y: 1 })).toBeTruthy();
    expect(isPositiveXY({ x: 1, y: -1 })).toBeFalsy();
    expect(isPositiveXY({ x: -1, y: 1 })).toBeFalsy();
    expect(isPositiveXY({ x: -1, y: -1 })).toBeFalsy();
  });
});

SemigroupAll을 μ΄μš©ν–ˆκΈ° λ•Œλ¬Έμ— semigroupPredicate μΈμŠ€ν„΄μŠ€μ˜ concat ν•¨μˆ˜μ— μ „λ‹¬λœ 두 ν•¨μˆ˜ λͺ¨λ‘ trueλ₯Ό λ°˜ν™˜ν•΄μ•Ό isPositiveXY ν•¨μˆ˜κ°€ trueλ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. x, y λͺ¨λ‘ 0 μ΄μƒμ˜ 값이 μ „λ‹¬λ˜μ—ˆμ„ 경우 trueκ°€ λ°˜ν™˜λ˜μ—ˆλŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.

Folding

μ •μ˜μ— 따라 concat은 A의 두 μš”μ†Œμ—μ„œλ§Œ μž‘λ™ν•©λ‹ˆλ‹€. 더 λ§Žμ€ μš”μ†Œλ₯Ό μ—°κ²°ν•˜λ €λ©΄ μ–΄λ–»κ²Œ ν•  수 μžˆμ„κΉŒμš”?

fold ν•¨μˆ˜λŠ” Semigroup μΈμŠ€ν„΄μŠ€, μ΄ˆκΉƒκ°’ 및 μš”μ†Œ 배열을 μ‚¬μš©ν•©λ‹ˆλ‹€.

μ›λ¬Έμ—μ„œλŠ” fold, semigroupSum, semigroupProductλ₯Ό μ‚¬μš©ν•˜λΌκ³  μž‘μ„±λ˜μ–΄ μžˆμ§€λ§Œ, μ΅œμ‹  λ²„μ „μ˜ fp-tsμ—μ„œλŠ” deprecated λ˜μ–΄ 있으며 concatAll, SemigroupSum, SemigroupProductλ₯Ό μ‚¬μš©ν•˜λ©΄ λ©λ‹ˆλ‹€.

import { SemigroupSum, SemigroupProduct } from 'fp-ts/lib/number';
import { concatAll } from 'fp-ts/lib/Semigroup';

const sum = concatAll(SemigroupSum);
const product = concatAll(SemigroupProduct);

μž‘μ„±ν•œ sum ν•¨μˆ˜μ™€ product ν•¨μˆ˜λŠ” μ•„λž˜μ™€ 같이 ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.

  • sum ν•¨μˆ˜λ₯Ό ν…ŒμŠ€νŠΈν•˜λŠ” μ½”λ“œ
describe('concatAll, SemigroupSumλ₯Ό μ‚¬μš©ν•œ sum ν•¨μˆ˜ ν…ŒμŠ€νŠΈ', () => {
  it('sumν•¨μˆ˜ ν…ŒμŠ€νŠΈ', () => {
    expect(sum(0)([1, 2, 3, 4])).toBe(10);
    expect(sum(10)([1, 2, 3, 4])).toBe(20);
  });
});
  • product ν•¨μˆ˜λ₯Ό ν…ŒμŠ€νŠΈν•˜λŠ” μ½”λ“œ
describe('concatAll, SemigroupProductλ₯Ό μ‚¬μš©ν•œ product ν•¨μˆ˜ ν…ŒμŠ€νŠΈ', () => {
  it('productν•¨μˆ˜ ν…ŒμŠ€νŠΈ', () => {
    expect(product(1)([1, 2, 3, 4])).toBe(24);
    expect(product(10)([1, 2, 3, 4])).toBe(240);
  });
});

μ›λ¬Έμ˜ fold ν•¨μˆ˜λŠ”μ™€ λ‹€λ₯΄κ²Œ concatAll ν•¨μˆ˜λŠ” μ΄ˆκΉƒκ°’μ„ 인자둜 λ°›κ³  concat을 μ‚¬μš©ν•  배열을 전달받아 값을 λ°˜ν™˜ν•˜λŠ” ν•¨μˆ˜λ₯Ό λ°˜ν™˜ν•œλ‹€. λ”°λΌμ„œ sum(0)([1, 2, 3, 4]), product(1)([1, 2, 3, 4]) 와 같이 ν•¨μˆ˜ 호좜 μ—°μ‚°μžλ₯Ό 두 번 μ‚¬μš©ν•΄ ν…ŒμŠ€νŠΈν•  수 μžˆλ‹€.

νƒ€μž… μƒμ„±μžλ₯Ό μœ„ν•œ Semigroup

Option<A> 두 개λ₯Ό β€œλ³‘ν•©β€ν•˜λ €λ©΄ μ–΄λ–»κ²Œ ν•  수 μžˆμ„κΉŒμš”? λ„€ 가지 κ²½μš°κ°€ μžˆμŠ΅λ‹ˆλ‹€.

x y concat(x, y)
none none none
some(a) none none
none some(a) none
some(a) some(b) ?

λ§ˆμ§€λ§‰ ν•˜λ‚˜μ— λ¬Έμ œκ°€ μžˆμŠ΅λ‹ˆλ‹€. 두 개의 Aνƒ€μž… some 객체λ₯Ό β€œλ³‘ν•©β€ν•˜λ €λ©΄ 무언가가 ν•„μš”ν•©λ‹ˆλ‹€.

두 개의 Aλ₯Ό β€œλ³‘ν•©β€ν•˜λŠ” 것이 Semigroup이 ν•˜λŠ” μΌμž…λ‹ˆλ‹€! A에 λŒ€ν•œ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό μš”κ΅¬ν•œ λ‹€μŒ Option<A>에 λŒ€ν•œ Semigroup μΈμŠ€ν„΄μŠ€λ₯Ό νŒŒμƒ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이것이 getApplySemigroup 콀비넀이터가 μž‘λ™ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.

μ›λ¬Έμ—μ„œλŠ” Option/getApplySemigroup, semigroupSum, λ₯Ό μ‚¬μš©ν•˜λΌκ³  μž‘μ„±λ˜μ–΄ μžˆμ§€λ§Œ, μ΅œμ‹  λ²„μ „μ˜ fp-tsμ—μ„œλŠ” deprecated λ˜μ–΄ 있으며 Apply/getApplySemigroup, SemigroupSumλ₯Ό μ‚¬μš©ν•˜λ©΄ λ©λ‹ˆλ‹€.

import { SemigroupSum } from 'fp-ts/lib/number';
import { getApplySemigroup } from 'fp-ts/lib/Apply';
import { Apply } from 'fp-ts/lib/Option';

const S = getApplySemigroup(Apply)(SemigroupSum);

μž‘μ„±ν•œ Option νƒ€μž…μ„ μ§€μ›ν•˜λŠ” Semigroup은 μ•„λž˜μ™€ 같이 ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.

describe('Optionνƒ€μž…μ„ μ§€μ›ν•˜λŠ” appliedSemigroup μΈμŠ€ν„΄μŠ€ ν…ŒμŠ€νŠΈ', () => {
  let result;
  it('appliedSemigroup ν…ŒμŠ€νŠΈ (some + none)', () => {
    result = appliedSemigroup.concat(some(1), none);
    expect(result).toBe(none);
    expect(isNone(result)).toBeTruthy();
  });
  it('appliedSemigroup ν…ŒμŠ€νŠΈ (some + some)', () => {
    result = appliedSemigroup.concat(some(1), some(2));
    expect(result).toMatchObject(some(3));
    expect(isSome(result)).toBeTruthy();
  });
});

some 객체와 none 객체λ₯Ό 더할 경우 none을 λ°˜ν™˜ν•˜λŠ”μ§€ ν™•μΈν•˜κ³  some 객체와 some 객체λ₯Ό 더할 경우 두 some 객체의 valueκ°€ 더해진 some 객체가 λ°˜ν™˜λ˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€.

뢀둝

Semigroup이 μ—¬λŸ¬ 데이터λ₯Ό ν•˜λ‚˜λ‘œ β€œμ—°κ²°β€, β€œλ³‘ν•©β€λ˜λŠ” β€œκ²°ν•©β€ν•˜κ³  싢을 λ•Œ λ„μ›€μ΄λ˜λŠ” 것을 λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

λ§ˆμ§€λ§‰ 예제(Fantas, Eel, Specification 4 : Semigroupμ—μ„œ μˆ˜μ •λ¨)둜 λͺ¨λ‘ λ§ˆλ¬΄λ¦¬ν•˜κ² μŠ΅λ‹ˆλ‹€.

μ•„λž˜μ™€ 같은 고객 정보λ₯Ό μ €μž₯ν•˜λŠ” μ‹œμŠ€ν…œμ„ κ΅¬μΆ•ν•œλ‹€κ³  κ°€μ •ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

interface Customer {
  name: string;
  favouriteThings: Array<string>;
  registeredAt: number;
  lastUpdatedAt: number;
  hasMadePurchase: boolean;
}

μ–΄λ–€ μ΄μœ λ‘œλ“  같은 μ‚¬λžŒμ— λŒ€ν•œ 쀑볡 기둝이 생길 수 μžˆμŠ΅λ‹ˆλ‹€. μš°λ¦¬μ—κ²Œ ν•„μš”ν•œ 것은 Semigroup이 ν•˜λŠ” 병합 μ „λž΅μž…λ‹ˆλ‹€.

μ›λ¬Έμ—μ„œ μ‚¬μš©ν•˜λŠ” νŒ¨ν‚€μ§€ 쀑 μ΅œμ‹  λ²„μ „μ˜ fp-tsμ—μ„œλŠ” deprecated λ˜μ–΄ μžˆλŠ” 것이 λ§Žμ•„ μ•„λž˜μ— λͺ©λ‘μœΌλ‘œ μž‘μ„±ν•˜κ² μŠ΅λ‹ˆλ‹€.

Deprecated Packages
import { Semigroup, struct, max, min } from 'fp-ts/lib/Semigroup';
import { getMonoid } from 'fp-ts/lib/Array';
import { Ord } from 'fp-ts/lib/number';
import { contramap } from 'fp-ts/lib/Ord';
import { SemigroupAny } from 'fp-ts/lib/boolean';

const semigroupCustomer: Semigroup<Customer> = struct({
  // 더 κΈ΄ 이름을 μœ μ§€ν•œλ‹€.
  name: max(contramap((s: string) => s.length)(Ord)),
  // ν•­λͺ©μ„ μΆ•μ ν•œλ‹€.
  // getMonoidλŠ” Semigroup을 μœ„ν•œ `Array<string>`을 λ°˜ν™˜ν•œλ‹€.
  favouriteThings: getMonoid<string>(),
  // κ°€μž₯ μ΄μ „μ˜ λ‚ μ§œλ₯Ό μœ μ§€ν•œλ‹€.
  registeredAt: min(Ord),
  // κ°€μž₯ 졜근의 λ‚ μ§œλ₯Ό μœ μ§€ν•œλ‹€.
  lastUpdatedAt: max(Ord),
  // λΆ„λ¦¬λœ boolean Semigroup
  hasMadePurchase: SemigroupAny,
});

μž‘μ„±ν•œ semigroupCustomer μΈν„°νŽ˜μ΄μŠ€λŠ” μ•„λž˜μ™€ 같이 ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€.

describe('Semigroup μΈν„°νŽ˜μ΄μŠ€λ₯Ό κ΅¬ν˜„ν•œ semigroupCustomer μΈμŠ€ν„΄μŠ€ ν…ŒμŠ€νŠΈ', () => {
  it('semigroupCustomer μΈμŠ€ν„΄μŠ€ concat ν•¨μˆ˜ ν…ŒμŠ€νŠΈ', () => {
    expect(
      semigroupCustomer.concat(
        {
          name: 'Giulio',
          favouriteThings: ['math', 'climbing'],
          registeredAt: new Date(2018, 1, 20).getTime(),
          lastUpdatedAt: new Date(2018, 2, 18).getTime(),
          hasMadePurchase: false,
        },
        {
          name: 'Giulio Canti',
          favouriteThings: ['functional programming'],
          registeredAt: new Date(2018, 1, 22).getTime(),
          lastUpdatedAt: new Date(2018, 2, 9).getTime(),
          hasMadePurchase: true,
        }
      )
    ).toMatchObject({
      name: 'Giulio Canti',
      favouriteThings: ['math', 'climbing', 'functional programming'],
      registeredAt: new Date(2018, 1, 20).getTime(),
      lastUpdatedAt: new Date(2018, 2, 18).getTime(),
      hasMadePurchase: true,
    });
  });
});

semigroupCustomer μΈν„°νŽ˜μ΄μŠ€μ˜ concat ν•¨μˆ˜λŠ” μ „λ‹¬λœ 두 개의 Customer νƒ€μž… 객체λ₯Ό λ³‘ν•©ν•œλ‹€. name 속성은 λ‘˜ 쀑 더 κΈ΄ κ²ƒμœΌλ‘œ μœ μ§€ν•˜κ³ , favouriteThings 속성은 두 속성을 ν•©μΉœλ‹€. registeredAt 속성은 λ‘˜ 쀑 더 μ΄μ „μ˜ μ‹œκ°„μ„ μœ μ§€ν•˜λ©° lastUpdatedAt 속성은 졜근 μ‹œκ°„μ„ μœ μ§€ν•˜κ³  hasMadePurchase 속성은 trueκ°€ 있으면 true둜 μœ μ§€ν•©λ‹ˆλ‹€.

λ”°λΌμ„œ 주어진 쑰건에 맞게 Customer νƒ€μž… 객체가 λ³‘ν•©λ˜μ—ˆλŠ”μ§€ 확인할 수 μžˆλ‹€.

getMonoid ν•¨μˆ˜λŠ” Array<string>에 λŒ€ν•œ Semigroup을 λ°˜ν™˜ν•©λ‹ˆλ‹€. μ‹€μ œλ‘œ monidλŠ” Semigroup μ΄μƒμ˜ 것을 λ°˜ν™˜ν•©λ‹ˆλ‹€.

κ·Έλž˜μ„œ monoidλŠ” λ¬΄μ—‡μΌκΉŒμš”? λ‹€μŒ ν¬μŠ€νŠΈμ—μ„œλŠ” Monoids에 λŒ€ν•΄ μ΄μ•ΌκΈ°ν•˜κ² μŠ΅λ‹ˆλ‹€.


Written by@Minsu Kim
Software Engineer at KakaoPay Corp.