← 목록으로
범주론 · 프로그래밍

모나드는 그냥 모노이드라니까

A monad is just a monoid in the category of endofunctors

실패할 수 있는 계산, 여러 답을 내는 계산, 상태를 끌고 가는 계산, 부수효과가 있는 계산 — 이 "맥락이 딸린 계산"들을 깔끔하게 이어 붙이는 단 하나의 패턴이 모나드입니다. 악명 높은 그 한 줄, "자기함자 범주의 모노이드"가 무슨 뜻인지까지 풀어 봅니다.

0문제 — 맥락이 딸린 계산을 잇기

보통 함수는 \(A\to B\)로 깔끔하게 이어집니다(\(f\) 다음 \(g\), 즉 \(g\circ f\)). 그런데 현실의 계산은 맥락을 달고 다닙니다. 결과가 없을 수도 있고(Option), 여럿일 수도 있고(List), 상태를 바꾸고(State), 바깥 세계에 영향을 주죠(IO). 이런 함수의 타입은 \(A\to M[B]\) 꼴입니다 — 결과 \(B\)가 상자 \(M\)에 담겨 나오는.

문제는 합성입니다. \(f:A\to M[B]\) 다음에 \(g:B\to M[C]\)를 그냥 \(g\circ f\)로 잇지 못합니다. \(f\)는 상자에 든 \(B\)를 주는데 \(g\)는 맨 \(B\)를 원하니까요. 이 어긋남을 메우는 보편적 장치가 모나드입니다.


1함자 복습 — 상자에 손을 넣는 map

먼저 함자(functor). 타입 생성자 \(M\)이 함자라는 건, 상자를 깨지 않고 안의 값에 함수를 적용하는 map을 가졌다는 뜻입니다:

$$ \texttt{map} : M[A]\times(A\to B)\to M[B]. $$

\(M[A]\)에 \(f:A\to B\)를 map하면 \(M[B]\)가 나오죠(List(1,2,3).map(_+1)). 그런데 \(f\)가 상자를 돌려주는 함수 \(A\to M[B]\)라면? map은 \(M[M[B]]\) — 상자 속 상자를 내놓습니다. 이 이중 상자를 어떻게 한 겹으로 펼 것인가, 그것이 다음 이야기입니다.


2모나드란 — pure와 flatMap

함자 \(M\)이 모나드가 되려면 두 가지 능력이 더 필요합니다.

모나드 = 함자 + pure + flatMap

flatMap은 사실 map으로 \(M[M[B]]\)를 만든 뒤 join(\(M[M[A]]\to M[A]\), 평탄화)으로 한 겹 벗기는 것과 같습니다. 즉 모나드를 보는 두 방식 — "flatMap 하나" 또는 "pure + join 둘" — 은 동치죠. 이제 어긋났던 합성이 풀립니다. \(f:A\to M[B]\), \(g:B\to M[C]\)를

$$ a \;\mapsto\; \texttt{flatMap}(f(a),\,g) $$

로 이으면 \(A\to M[C]\)가 깔끔하게 나옵니다.


3모나드 법칙 — 왜 필요한가

아무 pure·flatMap이나 모나드인 건 아닙니다. 셋의 법칙을 지켜야 "합성이 정상"입니다.

이 법칙들이 있어야 pure가 합성의 항등원 노릇을 하고, 긴 flatMap 사슬을 마음 놓고 재배열할 수 있습니다. 곧 보겠지만 이 셋은 그냥 "모노이드의 법칙"입니다.


4동물원 — Option · List · State · IO

아래는 Option 모나드가 실패를 어떻게 자동으로 전파하는지 보여 줍니다. \(x\)를 \(\frac1x\to\sqrt{\cdot}\to\ln(\cdot)\)로 흘려보내는데, 한 단계라도 정의되지 않으면 그 뒤는 전부 None으로 단락됩니다.

Option 사슬 — None은 저절로 번진다

x
\(x \to 1/x \to \sqrt{\cdot} \to \ln(\cdot)\)를 flatMap으로 이은 사슬. 초록 상자는 Some(값), 빨강은 None, 회색은 단락되어 실행조차 안 된 단계입니다. \(x=0\)(나눗셈)·음수(제곱근)·\(\sqrt{1/x}\le1\)(로그) 같은 데서 어떻게 무너지는지 보세요.

5클라이슬리 범주 — 효과 있는 함수의 합성

모나드의 정체를 가장 또렷이 보여 주는 그림이 클라이슬리 범주(Kleisli category)입니다. 대상은 그대로 타입이지만, 화살표를 \(A\to M[B]\)로 새로 정의합니다("효과를 내는 함수"). 두 화살표의 합성은 바로 flatMap으로 잇는 것이고, 항등 화살표는 pure죠.

그러면 앞 장의 모나드 세 법칙이 정확히 범주의 공리(합성의 결합법칙 + 항등원)로 번역됩니다. "모나드 법칙을 지켜라"는 곧 "클라이슬리 화살표들이 진짜 범주를 이루게 하라"는 말이었던 겁니다. 모나드란 결국 효과 있는 계산을 합성하는 새로운 범주를 만드는 장치입니다.


6"자기함자 범주의 모노이드"

이제 그 악명 높은 농담을 해독합니다. 모노이드란 결합적인 곱 \(\mu:M\otimes M\to M\)과 항등원 \(\eta:I\to M\)을 가진 대상입니다(정수의 덧셈, 문자열 잇기처럼). 그런데 "곱"이 꼭 숫자 곱일 필요는 없죠 — 임의의 모노이드 범주에서 정의됩니다.

한 범주의 자기함자(endofunctor)들을 모으면, 그 자체가 모노이드 범주가 됩니다. 대상은 함자, "곱 \(\otimes\)"는 함자 합성 \(\circ\), 단위 \(I\)는 항등함자죠. 이 무대에서 모노이드란 무엇일까요? 함자 \(M\)과 두 자연변환

$$ \mu : M\circ M \Rightarrow M \quad(\text{join}),\qquad \eta : \mathrm{Id}\Rightarrow M \quad(\text{pure}) $$

이고, 모노이드의 결합·단위 법칙은 정확히 앞서 본 모나드 법칙입니다. 그래서 — "모나드는 자기함자 범주에서의 모노이드일 뿐". 농담이지만 한 글자도 틀리지 않은 정의죠.

한눈 대조표
숫자 모노이드의 ↔ 함자 합성 · 항등원 1항등함자 · \(\mu:M\times M\to M\) ↔ join \(M\circ M\Rightarrow M\) · \(\eta\) ↔ pure \(\mathrm{Id}\Rightarrow M\). "곱"의 무대를 숫자에서 함자로 옮겼을 뿐, 구조는 똑같습니다.

7do 표기 — 프로그래밍의 세미콜론

현대 언어는 flatMap 사슬을 사람이 읽기 좋게 감싸 줍니다. 스칼라의 for 컴프리헨션, 하스켈의 do 표기죠. 아래 두 코드는 완전히 같습니다:

// Scala — flatMap 사슬
userOf(id).flatMap(u =>
  accountOf(u).flatMap(a =>
    balanceOf(a).map(b => b * 1.02)))

// Scala — for 컴프리헨션 (위와 동일)
for {
  u <- userOf(id)
  a <- accountOf(u)
  b <- balanceOf(a)
} yield b * 1.02

<- 줄은 "상자를 열어 값을 꺼낸다"는 한 번의 flatMap입니다. 그래서 모나드를 흔히 "프로그래밍 가능한 세미콜론"이라 부릅니다 — 문장과 문장 사이에서 "그다음 무엇을 하는가"(실패 전파, 상태 전달, 비동기 대기)를 모나드가 결정해 주니까요. Option이면 단락, Future면 비동기 연결, List면 모든 조합. 같은 문법, 다른 효과.

스칼라의 cats, 하스켈의 표준 라이브러리가 이 어휘(Functor·Monad)로 짜여 있고, 이는 앞선 분배법칙 페이지에서 본 범주론이 코드의 골격으로 그대로 내려온 모습입니다.


8맺으며

모나드는 어렵다기보다 낯선 이름이 붙은 익숙한 패턴입니다. "맥락이 딸린 계산을, 그 맥락을 일일이 손으로 풀지 않고 이어 붙이는 법." 그 한 패턴이 실패·비결정성·상태·부수효과를 같은 문법으로 길들이고, 그 정체는 자기함자 범주의 모노이드라는 한 줄로 닫힙니다. 범주론이 추상의 구름이 아니라 실무의 연장임을, 모나드만큼 잘 보여 주는 예도 드뭅니다.


참고 자료