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

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

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

μ§€λ‚œ ν¬μŠ€νŠΈμ—μ„œ μš°λ¦¬λŠ” M이 Applicative μΈμŠ€ν„΄μŠ€λ₯Ό μΈμ •ν•œλ‹€λ©΄ gλ₯Ό λ“€μ–΄ 올림으둜써 μˆœμˆ˜ν•œ nν•­ ν”„λ‘œκ·Έλž¨ g둜 μ΄νŽ™νŠΈ μžˆλŠ” ν”„λ‘œκ·Έλž¨ f: (a: A) => M<B>λ₯Ό ꡬ성할 수 μžˆμŒμ„ λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

ν”„λ‘œκ·Έλž¨ f ν”„λ‘œκ·Έλž¨ g μ‘°ν•©
μˆœμˆ˜ν•œ μˆœμˆ˜ν•œ g ∘ f
μ΄νŽ™νŠΈ μžˆλŠ” μˆœμˆ˜ν•œ, nν•­ liftAn(g) ∘ f

κ·ΈλŸ¬λ‚˜ λ§ˆμ§€λ§‰ ν•œ 가지 경우λ₯Ό ν•΄κ²°ν•΄μ•Ό ν•©λ‹ˆλ‹€. 두 ν”„λ‘œκ·Έλž¨μ΄ λͺ¨λ‘ μ΄νŽ™νŠΈκ°€ μžˆλ‹€λ©΄ μ–΄λ–»κ²Œ ν•  수 μžˆμ„κΉŒμš”?

f: (a: A) => M<B>
g: (b: B) => M<C>

그런 f와 g의 β€œμ‘°ν•©β€μ€ λ¬΄μ—‡μΌκΉŒμš”?

이 λ§ˆμ§€λ§‰ 경우λ₯Ό μ²˜λ¦¬ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ€‘μ²©λœ μ»¨ν…μŠ€νŠΈλ‘œ λλ‚˜κΈ° 쉽기 λ•Œλ¬Έμ— Functor보닀 더 κ°•λ ₯ν•œ 것이 ν•„μš”ν•©λ‹ˆλ‹€.

문제: μ€‘μ²©λœ μ»¨ν…μŠ€νŠΈ

더 λ§Žμ€ 것이 ν•„μš”ν•œ 이유λ₯Ό 더 잘 μ„€λͺ…ν•˜κΈ° μœ„ν•΄ λͺ‡ 가지 μ˜ˆμ‹œλ₯Ό μ‚΄νŽ΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

μ˜ˆμ‹œ (M = Array)

νŠΈμœ„ν„° μ‚¬μš©μžμ˜ νŒ”λ‘œμ›Œλ₯Ό κ²€μƒ‰ν•˜κ³  μ‹Άλ‹€κ³  κ°€μ •ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

interface User {
  followers: Array<User>;
}

const getFollowers = (user: User): Array<User> => user.followers;

const followersOfFollowers = (user: User): Array<Array<User>> =>
  getFollowers(user).map(getFollowers);

λ­”κ°€ 잘λͺ»λœ 것이 μžˆμŠ΅λ‹ˆλ‹€. followersOfFollowers ν•¨μˆ˜μ˜ λ°˜ν™˜ νƒ€μž…μ€ Array<Array<User>>μ΄μ§€λ§Œ μš°λ¦¬λŠ” Array<User> νƒ€μž…μ„ λ°˜ν™˜ν•˜κΈ°λ₯Ό μ›ν•©λ‹ˆλ‹€.

μš°λ¦¬λŠ” μ€‘μ²©λœ 배열을 ν‰ν‰ν•˜κ²Œ λ§Œλ“€μ–΄μ•Ό ν•©λ‹ˆλ‹€.

fp-tsμ—μ„œ μ œκ³΅ν•˜λŠ” flatten: <A>(mma: Array<Array<A>>) => Array<A> ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜λ©΄ νŽΈλ¦¬ν•©λ‹ˆλ‹€.

import { flatten } from 'fp-ts/lib/Array';

const followersOfFollowers = (user: User): Array<User> =>
  flatten(getFollowers(user).map(getFollowers));

μ’‹μŠ΅λ‹ˆλ‹€. λ‹€λ₯Έ 데이터 κ΅¬μ‘°λŠ” μ–΄λ–¨κΉŒμš”?

μ˜ˆμ‹œ (M = Option)

숫자 λͺ©λ‘μ˜ κ°€μž₯ μ•žμ˜ λ°μ΄ν„°μ˜ μ—­μˆ˜λ₯Ό κ³„μ‚°ν•˜κ³  μ‹Άλ‹€κ³  κ°€μ •ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€.

import { Option, some, none, option } from 'fp-ts/lib/Option';
import { head } from 'fp-ts/lib/Array';

const inverse = (n: number): Option<number> => (n === 0 ? none : some(1 / n));

const inverseHead = (arr: Array<number>): Option<Option<number>> =>
  option.map(head(arr), inverse);

λ‹€μ‹œ ν•œλ²ˆ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€. inverseHead ν•¨μˆ˜λŠ” Option<Option<number>> νƒ€μž…μ„ λ°˜ν™˜ν•˜κ³  μžˆμ§€λ§Œ μš°λ¦¬λŠ” Option<number> νƒ€μž…μ΄ λ°˜ν™˜λ˜κΈ°λ₯Ό μ›ν•©λ‹ˆλ‹€.

μš°λ¦¬λŠ” μ€‘μ²©λœ Option을 ν‰ν‰ν•˜κ²Œ λ§Œλ“€μ–΄μ•Ό ν•©λ‹ˆλ‹€.

import { isNone } from 'fp-ts/lib/Option';

const flatten = <A>(mma: Option<Option<A>>): Option<A> =>
  isNone(mma) ? none : mma.value;

const inverseHead = (arr: Array<number>): Option<number> =>
  flatten(option.map(head(arr), inverse));

λͺ¨λ“  flatten ν•¨μˆ˜λ“€μ€ μš°μ—°νžˆ 생긴 것이 μ•„λ‹™λ‹ˆλ‹€. 이것듀은 λͺ¨λ‘ μ•ˆμ— ν•¨μˆ˜ν˜•μ μΈ νŒ¨ν„΄μ΄ μ‘΄μž¬ν•©λ‹ˆλ‹€.

μ‹€μ œλ‘œ μ΄λŸ¬ν•œ λͺ¨λ“  νƒ€μž… μƒμ„±μž(및 기타 λ§Žμ€ μƒμ„±μž)λŠ” Monad μΈμŠ€ν„΄μŠ€λ₯Ό ν—ˆμš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

flatten은 Monad의 κ°€μž₯ κ³ μœ ν•œ κΈ°λŠ₯μž…λ‹ˆλ‹€.

κ·Έλž˜μ„œ MonadλŠ” λ¬΄μ—‡μΈκ°€μš”?

μ•„λž˜ λ‚΄μš©μ΄ Monadκ°€ 자주 μ œμ‹œλ˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.

μ •μ˜

MonadλŠ” μ•„λž˜μ™€ 같이 μ„Έ κ°€μ§€λ‘œ μ •μ˜λ©λ‹ˆλ‹€.

(1) Functor μΈμŠ€ν„΄μŠ€λ₯Ό ν—ˆμš©ν•˜λŠ” νƒ€μž… μƒμ„±μž M

(2) μ•„λž˜μ˜ μ‹œκ·Έλ‹ˆμ²˜λ₯Ό κ°–λŠ” of ν•¨μˆ˜

of: <A>(a: A) => HKT<M, A>

(3) μ•„λž˜ μ‹œκ·Έλ‹ˆμ²˜λ₯Ό κ°–λŠ” flatMap ν•¨μˆ˜

flatMap: <A, B>(f: (a: A) => HKT<M, B>) => ((ma: HKT<M, A>) => HKT<M, B>)

HKT νƒ€μž…μ€ μ œλ„€λ¦­ νƒ€μž… μƒμ„±μžλ₯Ό λ‚˜νƒ€λ‚΄λŠ” fp-ts 방식이며 HKT<M, X>λŠ” νƒ€μž… X에 적용된 νƒ€μž… μƒμ„±μž M (즉, M<X>)을 생각할 수 μžˆμŠ΅λ‹ˆλ‹€.

of와 flatMap ν•¨μˆ˜λŠ” μ•„λž˜μ˜ μ„Έ 가지 쑰건을 λ§Œμ‘±ν•΄μ•Ό ν•©λ‹ˆλ‹€.

  • μ™Όμͺ½ 항등식(Left identity): flatMap(of) ∘ f = f
  • 였λ₯Έμͺ½ 항등식(Right identity): flatMap(f) ∘ of = f
  • κ²°ν•© 법칙(Associativity): flatMap(h) ∘ (flatMap(g) ∘ f) = flatMap((flatMap(h) ∘ g)) ∘ f

μ—¬κΈ°μ„œ f, g, hλŠ” λͺ¨λ‘ μ΄νŽ™νŠΈκ°€ μžˆλŠ” ν•¨μˆ˜μ΄κ³  βˆ˜λŠ” 일반적인 ν•¨μˆ˜ μ‘°ν•©μž…λ‹ˆλ‹€.

μ’‹μŠ΅λ‹ˆλ‹€. 그런데 μ™œ?

이런 μ •μ˜λ₯Ό 처음 λ³΄μ•˜μ„ λ•Œ 첫 λ°˜μ‘μ€ λ‹Ήν™©ν–ˆμŠ΅λ‹ˆλ‹€.

μ•„λž˜μ˜ λͺ¨λ“  질문이 λ‚΄ λ¨Έλ¦Ώμ†μ—μ„œ 맴돌고 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

  • μ™œ κ·Έ 두 가지 νŠΉμ •ν•œ κΈ°λŠ₯을 ν•˜κ³  μ™œ 그런 νƒ€μž…μ„ κ°–κ³  μžˆλ‚˜μš”?
  • μ™œ 이름이 β€œflatMapβ€μΌκΉŒμš”?
  • μ™œ κ·œμΉ™λ“€μ΄ 있고 그것듀은 무엇을 μ˜λ―Έν• κΉŒμš”?
  • ν•˜μ§€λ§Œ 무엇보닀도 flatten은 어디에 μžˆμ„κΉŒμš”?

이 ν¬μŠ€νŠΈμ—μ„œλŠ” 각 μ§ˆλ¬Έμ— λŒ€ν•œ 닡변을 μ‹œλ„ν•©λ‹ˆλ‹€.

문제둜 λŒμ•„κ°€ λ³΄κ² μŠ΅λ‹ˆλ‹€. 두 개의 μ΄νŽ™νŠΈ μžˆλŠ” ν•¨μˆ˜(Kleisli arrows라고도 함)의 쑰합은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?

두 개의 Kleisli arrows의 쑰합은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?
두 개의 Kleisli arrows의 쑰합은 λ¬΄μ—‡μž…λ‹ˆκΉŒ?

λ‚˜λŠ” κ·Έκ²ƒμ˜ νƒ€μž…μ΄ 무엇인지쑰차 λͺ¨λ¦…λ‹ˆλ‹€.

μž κΉβ€¦ μš°λ¦¬λŠ” 이미 쑰합에 κ΄€ν•œ 좔상화λ₯Ό λ§Œλ‚¬μŠ΅λ‹ˆλ‹€. μΉ΄ν…Œκ³ λ¦¬μ— κ΄€ν•΄ μ–˜κΈ°ν•œ 것을 κΈ°μ–΅ν•˜κ³  μžˆμŠ΅λ‹ˆκΉŒ?

μΉ΄ν…Œκ³ λ¦¬λŠ” μ‘°ν•©μ˜ λ³Έμ§ˆμ„ ν¬μ°©ν•©λ‹ˆλ‹€.

μš°λ¦¬λŠ” 이 문제λ₯Ό μΉ΄ν…Œκ³ λ¦¬ 문제둜 λ°”κΏ€ 수 μžˆμŠ΅λ‹ˆλ‹€. Kleisli arrows의 μ‘°ν•© λͺ¨λΈμ— λ§žλŠ” μΉ΄ν…Œκ³ λ¦¬λ₯Ό 찾을 수 μžˆμŠ΅λ‹ˆκΉŒ?

Kleisli μΉ΄ν…Œκ³ λ¦¬

μ΄νŽ™νŠΈ μžˆλŠ” ν•¨μˆ˜λ§Œ ν¬ν•¨ν•˜λŠ” μΉ΄ν…Œκ³ λ¦¬ K (Kleisli μΉ΄ν…Œκ³ λ¦¬)λ₯Ό ꡬ성해 λ³΄κ² μŠ΅λ‹ˆλ‹€.

  • κ°μ²΄λŠ” TS μΉ΄ν…Œκ³ λ¦¬μ™€ λ™μΌν•œ 객체둜 즉 λͺ¨λ“  TypeScript νƒ€μž…μž…λ‹ˆλ‹€.
  • ν˜•νƒœλŠ” μ•„λž˜μ™€ 같이 κ΅¬μ„±λ©λ‹ˆλ‹€. TS에 Kleisli ν™”μ‚΄ν‘œ f: A ⟼ M<B>κ°€ 있으면 Kμ—μ„œ ν™”μ‚΄ν‘œ f': A ⟼ B​λ₯Ό κ·Έλ¦½λ‹ˆλ‹€.
TS μΉ΄ν…Œκ³ λ¦¬ μœ„, K μƒμ„±μž μ•„λž˜
TS μΉ΄ν…Œκ³ λ¦¬ μœ„, K μƒμ„±μž μ•„λž˜

κ·Έλ ‡λ‹€λ©΄ Kμ—μ„œ f'와 g'의 쑰합은 μ•„λž˜ μ΄λ―Έμ§€μ—μ„œ h'라고 ν‘œμ‹œλœ 점선 ν™”μ‚΄ν‘œμž…λ‹ˆλ‹€.

TS μΉ΄ν…Œκ³ λ¦¬μ˜ μ‘°ν•© μœ„, K μƒμ„±μžμ˜ μ‘°ν•© μ•„λž˜
TS μΉ΄ν…Œκ³ λ¦¬μ˜ μ‘°ν•© μœ„, K μƒμ„±μžμ˜ μ‘°ν•© μ•„λž˜

h'λŠ” Aμ—μ„œ C둜 μ΄λ™ν•˜λŠ” ν™”μ‚΄ν‘œμ΄λ―€λ‘œ TSμ—λŠ” Aμ—μ„œ M<C>κΉŒμ§€μ— ν•΄λ‹Ήν•˜λŠ” ν•¨μˆ˜ hκ°€ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

λ”°λΌμ„œ TSμ—μ„œ f와 g의 쑰합에 λŒ€ν•œ 쒋은 ν›„λ³΄λŠ” μ—¬μ „νžˆ (a: A) => M <C>의 μ‹œκ·Έλ‹ˆμ²˜λ₯Ό 가진 μ΄νŽ™νŠΈ μžˆλŠ” ν•¨μˆ˜μž…λ‹ˆλ‹€.

κ·ΈλŸ¬ν•œ ν•¨μˆ˜λ₯Ό μ–΄λ–»κ²Œ ꡬ성 ν•  수 μžˆμ„κΉŒμš”?

μš°λ¦¬λŠ” λ‹¨κ³„λ³„λ‘œ 쑰합을 κ΅¬μ„±ν•©λ‹ˆλ‹€.

Monad μ •μ˜μ˜ μš”μ (1)은 M이 Functor μΈμŠ€ν„΄μŠ€λ₯Ό ν—ˆμš©ν•˜λ―€λ‘œ ν•¨μˆ˜ g: (b: B) => M<C>λ₯Ό ν•¨μˆ˜ lift(g): (mb: M<B>) => M<M<C>>둜 λ“€μ–΄ 올릴 수 μžˆμŠ΅λ‹ˆλ‹€. (μ—¬κΈ°μ„œλŠ” λ™μ˜μ–΄ map을 μ‚¬μš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.)

flatMap은 μ—¬κΈ°μ„œ μœ λž˜λœλ‹€.
flatMap은 μ—¬κΈ°μ„œ μœ λž˜λœλ‹€.

그리고 이제 μš°λ¦¬λŠ” λ§‰ν˜”μŠ΅λ‹ˆλ‹€. M<M<C>> νƒ€μž…μ˜ 값을 M<C> νƒ€μž…μ˜ κ°’μœΌλ‘œ ν‰ν‰ν•˜κ²Œ λ§Œλ“€ 수 μžˆλŠ” Functor μΈμŠ€ν„΄μŠ€μ— λŒ€ν•œ κΈ°λŠ₯이 μ—†μŠ΅λ‹ˆλ‹€. 좔가적인 flatten κΈ°λŠ₯이 ν•„μš”ν•©λ‹ˆλ‹€.

μ΄λŸ¬ν•œ κΈ°λŠ₯을 μ •μ˜ ν•  수 μžˆλ‹€λ©΄ μ°Ύκ³  μžˆλŠ” 쑰합을 얻을 수 μžˆμŠ΅λ‹ˆλ‹€.

h = flatten ∘ map(g) ∘ f

ν•˜μ§€λ§Œ flatten ∘ map(g)은 flatMapμž…λ‹ˆλ‹€. 이름은 μ—¬κΈ°μ—μ„œ μœ λž˜λ˜μ—ˆμŠ΅λ‹ˆλ‹€!

h = flatMap(g) ∘ f

이제 β€œμ‘°ν•©ν‘œβ€λ₯Ό μ—…λ°μ΄νŠΈ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

ν”„λ‘œκ·Έλž¨ f ν”„λ‘œκ·Έλž¨ g μ‘°ν•©
μˆœμˆ˜ν•œ μˆœμˆ˜ν•œ g ∘ f
μ΄νŽ™νŠΈ μžˆλŠ” μˆœμˆ˜ν•œ, nν•­ liftAn(g) ∘ f
μ΄νŽ™νŠΈ μžˆλŠ” μ΄νŽ™νŠΈ μžˆλŠ” flatMap(g) ∘ f

ofλŠ” μ–΄λ–€κ°€μš”? ofλŠ” K의 ν•­λ“±μ„± ν˜•νƒœμ—μ„œ μœ λž˜λ©λ‹ˆλ‹€. K의 각 ν•­λ“±μ„± ν˜•νƒœμ— λŒ€ν•΄ Aμ—μ„œ M<A>κΉŒμ§€μ— ν•΄λ‹Ήν•˜λŠ” ν•¨μˆ˜κ°€ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€ (즉, of: <A>(a: A) => M<A>)

ofλŠ” μ—¬κΈ°μ„œ μœ λž˜λœλ‹€.
ofλŠ” μ—¬κΈ°μ„œ μœ λž˜λœλ‹€.

법칙

λ§ˆμ§€λ§‰ 질문: 법칙은 μ–΄λ””μ—μ„œ μ™”μŠ΅λ‹ˆκΉŒ? 그것듀은 TS둜 λ²ˆμ—­λœ K의 μΉ΄ν…Œκ³ λ¦¬ 법칙일 λΏμž…λ‹ˆλ‹€.

법칙 K TS
μ™Όμͺ½ 항등식 (Left identity) 1B ∘ f’ = f’ flatMap(of) ∘ f = f
였λ₯Έμͺ½ 항등식 (Right identity) f' ∘ 1A = f' flatMap(f) ∘ of = f
κ²°ν•© 법칙 (Associativity ) h' ∘ (g' ∘ f') = (h' ∘ g') ∘ f' flatMap(h) ∘ (flatMap(g) ∘ f) = flatMap((flatMap(h) ∘ g)) ∘ f

fp-ts의 Monad

fp-tsμ—μ„œ flatMap ν•¨μˆ˜λŠ” chainμ΄λΌλŠ” λ³€ν˜•μ— μ˜ν•΄ λͺ¨λΈλ§ 되며, 기본적으둜 μΈμˆ˜κ°€ μž¬λ°°μ—΄ 된 flatMap μž…λ‹ˆλ‹€.

flatMap: <A, B>(f: (a: A) => HKT<M, B>) => ((ma: HKT<M, A>) => HKT<M, B>)
chain:   <A, B>(ma: HKT<M, A>, f: (a: A) => HKT<M, B>) => HKT<M, B>

μ°Έκ³ : chain은 flatMapμ—μ„œ νŒŒμƒλ  수 있으며 λ°˜λŒ€λ„ κ°€λŠ₯ν•˜λ‹€.

이제 μ€‘μ²©λœ μ»¨ν…μŠ€νŠΈμ˜ 문제λ₯Ό λ³΄μ—¬μ£ΌλŠ” 예제둜 λŒμ•„κ°€ chain을 μ‚¬μš©ν•˜μ—¬ 문제λ₯Ό ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

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

import type { Option } from 'fp-ts/lib/Option';
import { chain as arrayChain, head } from 'fp-ts/lib/Array';
import { chain as optionChain } from 'fp-ts/lib/Option';

const followersOfFollowers = (user: User): Array<User> =>
  arrayChain(getFollowers)(getFollowers(user));

const headInverse = (arr: Array<number>): Option<number> =>
  optionChain(inverse)(head(arr));

κ²°λ‘ 

ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ€ μ΄νŽ™νŠΈλ‘œ ν•¨μˆ˜λ₯Ό μ‘°ν•©ν•˜λŠ” 보편적인 방법을 μ œκ³΅ν•©λ‹ˆλ‹€. Functor, Applicative Functor 및 MonadλŠ” λͺ¨λ‘ λ‹€λ₯Έ μ’…λ₯˜μ˜ ν”„λ‘œκ·Έλž¨μ„ μ‘°ν•©ν•˜κΈ° μœ„ν•œ 원칙적인 도ꡬλ₯Ό μ œκ³΅ν•˜λŠ” μΆ”μƒν™”μž…λ‹ˆλ‹€.

μš”μ•½ : ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ°μ€ μ‹€μ œλ‘œ 쑰합에 κ΄€ν•œ κ²ƒμž…λ‹ˆλ‹€.


Minsu Kim
Written by@Minsu Kim
Software Engineer at KakaoPay Corp.