그래서 모나드가 뭔데? 타입 확장과 함수 합성의 관점에서 바라보기

함수형 프로그래밍을 공부하다보면 모나드라는 용어를 가끔씩 들어볼 수 있습니다. 구글에 검색해보면 모나드를 설명하는 좋은 블로그 글들을 많이 찾아볼 수 있습니다. 저도 여기에 제가 이해한 내용을 한 줌 보태려 합니다.

사실 모나드에 대해 정확히 이해하지 않아도 함수형 프로그래밍 패러다임을 실무에 적용하는데 큰 어려움은 없습니다. 그래도 알고 공부하면 더 재밌으니 제가 이해한 모나드 개념을 함수 합성과 타입 확장의 관점에서 설명해보겠습니다.

TL;DR

  1. 프로그램을 거대한 합성 함수로 기술하고 싶다.
    • 프로그래밍 맥락의 함수가 아닌 수학적으로 순수한 함수로 기술하고 싶다.
  2. 하지만 수학 함수는 현실 세계의 문제를 표현하는 능력이 부족하다.
    • 수학 함수로는 nullability, nondeterminism, exceptions, IO 등을 표현할 수 없다.
  3. 함수의 표현력을 늘리기 위해 타입을 확장하여 새로운 타입을 만드는 타입 생성자를 도입한다.
    • 예) Int: 숫자, Optional<Int>: null이 될 수 있는 숫자, Optional이 타입 생성자
  4. 타입을 확장했더니 함수끼리 합성하기가 어려워졌다.
    • 합성의 조건: 앞 함수의 리턴 타입과 뒷 함수의 파라미터 타입이 일치해야 한다.
    • 예) f: A -> M<B>g: B -> M<C> 는 합성할 수 없다. (M은 타입 생성자)
  5. 함수 합성을 쉽게 하기 위해 두 개의 특별한 함수를 추가한다.
    • bind(혹은 flatMap): M<A> -> (A -> M<B>) -> M<B>
    • return(혹은 pure 혹은 constructor): A -> M<A>
  6. 타입 생성자 M, bind, return 3개를 묶어서 Kleisli Triple 이라 한다. Kleisli Triple은 카테고리 이론에서 정의하는 모나드와 일대일 대응(동치)이다. 따라서 이를 편의상 모나드라 부른다.
  7. 모나드를 하스켈에서는 타입 클래스로, 스칼라에서는 제네릭 트레이트(trait)와 implicit으로 표현한다. 자바에서는 모나드 인스턴스를 제네릭 클래스, 생성자, flatMap 조합으로 표현할 수 있다.

이 글은 Moggi, E. (1991)을 읽고 제 해석을 가미해 정리한 글입니다.

순수 함수만으로 프로그램 짜기 정말 가능한가?

함수형 프로그래밍을 처음 공부하면, 그 이름에 걸맞게 순수 함수가 무엇이고, 프로그래밍 맥락의 함수와 수학 맥락의 함수의 차이점에 대해 설명하면서 시작합니다. 덧붙여 순수 함수만을 이용해 프로그램을 짜면 디버깅, 유지보수가 쉽고, 어쩌구 저쩌구 여러 장점을 늘어놓습니다.

  • 순수 함수의 조건
    1. 모든 입력에 대해 출력이 정의된다. (total function)
    2. 입력에 대해 출력이 결정적이다. (deterministic)
    3. 외부 상태(전역 변수, 파일, 데이터베이스 등)를 변경하거나 외부와 상호작용하지 않아야 한다. (no side effects)

그런데 곰곰이 생각해보면 순수 함수만으로 프로그램을 짜는 게 가능할까요? “hello, world!”를 콘솔에 출력하는 프로그램을 만든다고 할 때, 콘솔에 출력하는 행위 자체가 부수 효과인데 어떻게 부수 효과가 없는 순수 함수만으로 프로그램을 짤 수 있을까요?

정답은… 짤 수 없습니다. 부수 효과가 필요한 작업을 포함한 프로그램에는 반드시 부수 효과를 일으킬 순수하지 않은 함수를 실행해야 합니다. 함수형 프로그래밍이 제시하는 방향은 전체 프로그램의 대부분(느낌 상 99.9%)을 순수한 함수로 작성하고, 부수 효과를 가장 마지막에 터뜨리는 것입니다.

“hello, world!” 출력 프로그램을 예시로 이해하기 쉽게 쓰면 아래와 같습니다. (브라우저로 실행할 수 있으니 자바스크립트로 작성했습니다.)

// 프로그램의 대부분은 아래와 같이 순수한 함수들의 연결로 표현합니다.
const join = (a: string, b: string) => a + ", " + b
// print는 문자열을 받아 함수를 반환하므로 순수한 함수입니다.
const print = (message: string) => {
return () => { console.log(message) }
}
// 함수 합성
const program = print(join("hello", "world!"))
// 부수 효과를 한 번에 터뜨립니다.
program()
view raw fp.js hosted with ❤ by GitHub

분명 마지막 줄에서 부수 효과가 발생했습니다. 하지만 그 직전에 program 변수가 완성될 때까지는 아무런 부수효과도 발생하지 않습니다. 이게 함수형 프로그래밍이 지향하는 방식입니다.

프로그램 안에 program이란 변수를 쓴 것은 의도한 바입니다. 함수형 프로그래밍은 결국 program을 수학적으로 순수하면서 거대한 합성 함수로 만드는 과정입니다. (🤔 이 목표를 한 켠에 기억해주세요. 뒤에서 다시 나옵니다…)

순수 함수 표현력의 한계

앞선 코드 예시를 통해 함수형 프로그래밍에서 작성하고자 하는 프로그램이 최종적으로 어떤 형태가 될지 알게 되었습니다. 하지만 여전히 수학적으로 순수한 함수들만으로는 표현하기 어려운 동작(혹은 연산)이 존재합니다.

  • 결과 값이 없을 수 있는 경우 (Nullability, Partiality)
  • 계산이 실패할 수 있는 경우 (Exceptions)
  • 결과가 여러 개일 수 있는 경우 (Non-determinism)
  • 프로그램 외부 세계와 소통해야 하는 경우 (IO)
  • 부수 효과가 발생하는 경우 (Side-effects)

이전 코드 예시에서 콘솔에 입출력하는 등의 IO 동작은 함수로 표현하여 부수 효과를 지연시켰습니다. 다른 동작들은 어떻게 수학적으로 순수한 함수로 표현할까요?

아이디어: 값에 ‘컨텍스트’를 입히자, 타입 생성자로 확장

이 문제를 해결하기 위해 프로그래머들은 멋진 아이디어를 떠올립니다. 바로 값을 컨텍스트(Context)로 감싸는 것입니다. 구체적으로 타입 생성자 M을 이용해 IntM<Int>로 확장합니다. 이제 M<Int>는 단순한 정수 타입이 아니라 특별한 의미를 가집니다.

  • Optional<Int> (Java) - 컨텍스트: 값이 있을 수도, 없을 수도 있음 (Nullability, Partiality)
  • Result<Int> (Kotlin) - 컨텍스트: 예외가 발생할 수 있음 (Exceptions)
  • List<Int> - 컨텍스트: 비결정적임, 여러 결과가 나올 수 있음 (Non-determinism)
    • 참고) 일반적으로 List는 값이 여러 개일 때 사용하지만, 비결정적인 결과를 표현할 때 사용할 수도 있습니다. Nondeterministic Finite Automata에 대해 아시는 분이라면, 이를 List를 이용하여 쉽게 구현할 수 있습니다.
  • IO<Int> - 컨텍스트: 외부 세계와 상호작용함 (IO)
  • State<Int> - 컨텍스트: 부수 효과가 발생하여 외부 상태가 변경될 수 있음 (Side-effects)

말장난처럼 보입니다. 기존 타입 앞에 몇 글자 썼다고 순수하지 않은 함수가 순수한 함수가 되었다고 말할 수 있을까요?

예를 들어 나누기 함수를 구현한다고 해보겠습니다. 평범하게 함수를 구현한다면 Int, Int -> Int 타입이 됩니다. 하지만 나눗셈은 두 번째 인자로 0이 들어왔을 때 결과를 계산할 수 없으므로 partial function 입니다. 이를 total function으로 만들기 위해 함수 타입을 Int, Int -> Optional<Int> 로 쓸 수 있습니다.

타입을 값의 집합이라고 본다면, Int는 모든 정수 집합이 되고, Optional<Int>는 정수 집합에 EMPTY 원소(혹은 bottom )가 추가된 집합이라 볼 수 있습니다. 따라서 새롭게 확장한 함수는 수학적으로 순수성을 지키면서 기존에 표현하지 못했던 Partiality를 표현할 수 있게 되었습니다.

부수효과가 있는 함수도 IO 타입 생성자나 State 타입 생성자로 순수 함수로 바꿀 수 있습니다. 다만 해당 예시는 설명이 너무 길어져 생략하겠습니다.

새로운 문제: 함수 합성이 깨지다

순수하지 않은 함수를 순수하게 만들었으니 모든 문제가 해결되었을까요? 다시 함수형 프로그래밍의 목표를 떠올려보겠습니다.

우리는 수학적 순수성을 유지하면서 작은 함수들을 합성해 거대한 최종 함수를 만들어야 합니다. 타입 생성자 도입을 통해 표현력이 커진 순수 함수를 만들었으나 함수 합성에서 문제가 생겼습니다.

두 함수 f, g를 합성할 수 있으려면 f의 치역이 g의 정의역에 포함되어야 합니다. 프로그래밍 관점으로 바꿔 말하면, f의 반환 타입이 g의 인자 타입과 같거나 서브타입이어야 합니다. 그런데 타입 생성자를 통해 반환 타입을 확장하면서 기존에 합성이 가능했던 함수들이 합성할 수 없게 되었습니다.

  • 기존: f: A -> Bg: B -> C ⇒ 합성 가능 f•g: A -> C
    • 원래 g◦f 라고 쓰는 것이 맞지만 보기 편하게 f•g 라고 쓰겠습니다.
  • 확장 후: f: A -> M<B>g: B -> M<C> ⇒ 합성 불가

f의 결과물인 M<B>는 g의 입력인 B와 타입이 맞지 않습니다. 파이프가 중간에 깨져버린 셈이죠. 우리는 M<B>에서 B를 안전하게 꺼내 g에 전달하고, 그 결과인 M<C>를 최종 결과로 만들어 줄 새로운 ‘접착제’가 필요합니다.

해결책: bind와 return이라는 접착제

이 깨진 합성을 이어주기 위해 두 가지 특별한 함수가 등장합니다. 바로 bindreturn입니다.

  1. return: A -> M<A> (혹은 pure 혹은 생성자)
    • 평범한 값을 컨텍스트에 담아 확장하는 함수입니다.
    • 자바와 같은 OOP 언어에서 생성자(constructor)와 같습니다.
  2. bind: M<A> -> (A -> M<B>) -> M<B> (혹은 >>= 혹은 flatMap)
    • 컨텍스트 속의 값을 꺼내 함수에 적용한 뒤, 다시 컨텍스트에 담아주는 핵심 접착제입니다.
    • 함수 타입이 복잡해보이지만 자바, 자바스크립트 등 널리 쓰이는 언어에서도 함수형 패러다임을 적용할 때 쉽게 볼 수 있는 flatMap과 같습니다.

바로 전 단계에서 함수를 확장하여 f: A -> M<B>, g: B -> M<C> 와 같이 바뀌었습니다. bind 함수는 f 함수의 결과 값 M<B>g: B -> M<C> 함수 2개를 인자로 받아 안전하게 연결한 후 g 함수의 결과 타입인 M<C>를 내놓습니다.

이제 우리는 bind를 이용해 타입 생성자로 확장된 함수들을 체인처럼 엮을 수 있게 되었습니다.

규칙: 믿고 쓸 수 있는 접착제를 위한 ‘모나드 법칙’

새롭게 제시한 bind와 return 함수는 타입만 있을 뿐 구체적인 구현이 빠져있습니다. 그럼 함수 타입만 만들면 아무렇게나 함수를 만들어도 되는걸까요?

bind와 return을 통해 “올바르게” 함수를 합성하려면 3가지 법칙을 반드시 만족해야 합니다. 이를 모나드 법칙(Monad Laws)이라고 부릅니다.

  1. 좌 항등원 (Left Identity)
    • return이 bind 함수의 좌 항등원이어야 합니다.
    • return으로 감싼 값과 f를 bind 함수에 적용한 결과는 f에 그냥 값을 적용한 것과 같아야 합니다.
    • bind(return(a), f) == f(a)
  2. 우 항등원 (Right Identity)
    • return이 bind 함수의 우 항등원이어야 합니다.
    • 컨텍스트 속의 값과 return을 bind에 적용한 결과는 원래 컨텍스트 속 값과 같아야 합니다.
    • bind(m, return) == m
  3. 결합 법칙 (Associativity)
    • bind 함수에 대해 결합 법칙을 만족해야 합니다.
    • bind를 2번 연달아 사용해 연결할 때 앞 부분을 먼저 연결하든 뒷 부분을 먼저 연결하든 결과가 같아야 합니다.
    • bind(bind(m, f), g) == bind(m, x -> bind(f(x), g))

위 3가지 법칙을 만족하는 bind와 return이라면 어떤 구현이든 안전하게 함수 합성에 사용할 수 있습니다. 왜 안전한지에 대해서는 논문에서 자세히 설명하지만 여기까지는 진짜 몰라도 개발하는데 지장이 없기 때문에 넘어가겠습니다. (사실 저도 설명할 정도로 이해하지 못했습니다. 🤣 우리가 직접 모나드를 구현할 일은 거의 없으니 가볍게 넘어가셔도 괜찮습니다.)

결론: 그래서 모나드가 뭔데?

지금까지 프로그램을 수학적으로 순수한 거대한 합성함수를 만들기 위한 여정이었습니다. 값에 컨텍스트를 입혀 프로그래밍 세계의 표현력을 부여하는 타입 생성자 M, 확장된 함수를 합성할 때 도움을 주는 bindreturn까지 이 셋의 조합을 “Kleisli triple” 이라고 합니다. 그리고 Kleisli triple이 카테고리 이론에서 정의하는 Monad와 수학적으로 일대일 대응(동치) 관계라고 합니다. 그래서 편의상 이 셋을 묶어서 모나드라고 합니다.

이 모나드를 표현하는 방법은 언어마다 다릅니다. 자바와 같은 OOP 언어에서는 아쉽게도 “모나드”를 표현할 수 없습니다. 대신 타입 파라미터를 받을 수 있는 제네릭 클래스, 생성자, flatMap 함수를 통해 모나드 인스턴스(List, Optional, CompletableFuture 등)를 표현합니다. (비유하자면 싱글톤 오브젝트를 직접 만들 수 있지만 클래스 문법은 지원하지 않는 언어를 생각하시면 됩니다.)

함수형 프로그래밍 언어로 유명한 하스켈은 타입 클래스를 통해 모나드를 표현합니다. 자바와 비슷하지만 함수형 패러다임을 염두에 두고 디자인된 언어인 스칼라는 implicit 기능을 통해 하스켈의 타입 클래스를 따라할 수 있고, 이를 통해 모나드를 표현할 수 있습니다.

프로그래머들은 수학적으로 검증된 이론을 바탕으로 모나드를 이용해 부수 효과를 안전하고 우아하게 다룰 수 있게 되었습니다. 함수형 프로그래밍에서 모나드는 단순한 디자인 패턴을 넘어, 계산을 추상화하는 아름다운 도구인 셈입니다.

마치며

모나드라는 용어를 처음 들은 것이 2017년 쯤이었던 것 같습니다. 당시 저는 Lisp의 방언 프로그래밍 언어인 Racket을 학교 수업에서 배워 함수형 프로그래밍에 대해 처음 알게 되었고, Scala를 처음 써보기 시작했습니다. 그래서 모나드 개념이 전혀 이해되지 않았고, 이걸 알아야 하는지 몰라도 되는지조차 가늠하기 어려웠습니다.

이 글의 핵심 아이디어가 되는 논문인 Moggi, E. (1991)는 2019년 군대에서 읽게 되었습니다. 정말 운이 좋게도 제가 있던 부대에 하스켈을 잘 쓰는 친구가 들어와 함수형 프로그래밍을 함께 공부할 기회가 있었습니다. 마침 영어도 (매우) 잘하고 수학도 잘해서 함께 논문을 스터디하며 모나드에 대한 개념을 엄밀하진 않지만 제 안에서 구축할 수 있었습니다.

사실 함께 스터디했다기보다 거의 배우는 수준이었습니다. 신기하게 심심할 때 현대 대수학(abtract algebra), 카테고리 이론, 하스켈 그리고 한자(?! 중국어 아님)를 공부하는 후임이었습니다. 같이 이야기하면 공부하는 재미가 있어 참 즐거웠던 기억이 있네요.

당시에는 자유로운 시간이 없다고 느꼈는데 지금은 더 많은 자유를 가졌으나 정작 무언가에 몰입해서 재미있게 공부한지 오랜 시간이 지난 것 같습니다. 무언가를 배울 때 미래의 나에게 어떤 이득이 있을까에 따지게 되고, AI가 더 잘한다는 핑계를 대다보니 시작도 어려워지는 것 같습니다.

우연한 기회로 한동안 손 놓았던 함수형 프로그래밍을 다시 공부하게 되어 기록도 남기는 겸 글을 쓰게 되었습니다. 함수형 프로그래밍을 다시 잡고 싶은 생각은 없지만 그 때 열심히 했던 기억을 다시 살려서 재밌는 공부를 해보고 싶어졌습니다.

References

  1. Moggi, E. (1991). Notions of computation and monads. Information and computation, 93(1), 55-92.
  2. Wadler, P. (1989, November). Theorems for free!. In Proceedings of the fourth international conference on Functional programming languages and computer architecture (pp. 347-359).
comments powered by Disqus

관련문서