JS의 Promise, 파서 콤비네이터, Optional/Result, CPS, LINQ 같은 익숙한 체이닝 직관을 통해 모나드가 무엇인지, 왜 하스켈에서 특히 유용한지 간단히 풀어 설명한다.
“모나드 설명을 이해한 건 이번이 처음입니다.” – 댓글 작성자
“방금 당신의 설명을 읽고 나니, 갑자기 모나드를 이해한 것 같습니다.” – 리뷰어
모나드는 하스켈 프로그래밍 언어에서의 사용과 함께 프로그래밍 세계에서 악명 높고, 이해하기 어렵다고 알려져 있습니다. “모나드 튜토리얼을 쓰는 일”이 새로운 하스켈러의 통과의례라는 농담이 있을 정도이고, 심지어 무의미하다고까지 묘사되곤 합니다.1 저는 하스켈을 10년 넘게 써 왔지만, 또 하나의 모나드 튜토리얼을 쓰는 일은 삼가 왔습니다.
한 친구가 하스켈 코드를 쓰지 않고 쉽게 설명해 달라고 했습니다.2 그건 더 쉽죠.
다른 대중적 언어에서 흔히 하는 체이닝에 대한 당신의 직관을 그대로 재사용하면 됩니다:
90년대 하스켈 설계자 중 한 명이3 이 모든 것에 공통으로 맞는 클래스/인터페이스를 고안했습니다. 그가 범주론에 관심이 있었기에 이를 “모나드”라는 개념과 연관지었고, 타입들도 눈을 가늘게 뜨고 보면 이 이론과 얼추 맞아떨어집니다.
그리고 그 클래스 메서드를 호출하는 일을 더 인간공학적으로 느끼게 해 주는 문법(사실 두 가지4)을 만들었습니다. 즉, 문법 설탕이죠.
파서, CPS, 비동기, 옵셔널 체이닝은 대부분의 현대 대중 언어에서 대체로 다음과 같이 생겼습니다:
getThing.and_then(x => putThingElsewhere(x+4)).and_then(..)
파서의 경우 문자열 내 위치를 추적하고 그 상태를 계속 흘려보냅니다. “옵셔널”의 경우 and_then
의 왼쪽이 nil/null/undefined이면 그냥 단락 평가합니다. 비동기의 경우 웹 요청처럼 비동기 작업을 수행하고, 완료되면 콜백을 호출하는 식이죠.
모나드는 이렇게 정의된 “and_then”5을 위한 클래스의 이름이며, 예측 가능한 동작을 위한 몇 가지 법칙(법칙들)이 있고, 그러면 이 “and_then”을 구현하는 무엇이든 동작하는 수많은 라이브러리 코드가 존재합니다.
F#이나 하스켈(또는 그 자손들)을 제외하면, 다른 언어들6은 이 추상화를 문법 차원에서 받아들이지 않기 때문에, 그런 문법 없이 좋은 설명을 찾기가 어렵습니다. 마치 Lisp 없이 Lisp 매크로를 설명하는 것과 같아서, 설명이 어색하고 설득력이 떨어지기 마련입니다.
가변 상태가 있다면 파서를 위해 굳이 상태 배관질을 하지 않고, 문자열 위치를 제자리에서 갱신하고 파싱 오류에는 예외를 던질 수도 있습니다. 함수에서 조기 반환을 지원하는 언어라면 그것으로 단락 평가를 할 수 있습니다(Rust가 그렇게 합니다). 언어가 비동기에 대해 “await” 같은 특별 문법을 제공한다면(JS, Rust, C#), 그냥 그걸 쓰면 됩니다. 이런 다른 방식들이 존재하면 모나드라는 추상화가 다소 불필요해집니다.
하스켈러들은 예외를 던지는 것을 좋아하지 않고, 가변 상태도 쓰지 않으며, 함수는 조기 반환을 할 수도 없습니다. 그러다 보니 모나드와 그에 대한 문법 설탕이 그들에게는 꽤 매력적으로 보입니다.
이해합니다. 대부분의 설명이 하스켈 타입을 활용하니까요. 위키피디아의 글은 형편없습니다.↩︎
do-표기법 또는 리스트 내포(맞아요, 파이썬의 그것처럼)로, 이를 일반화하면 모나드 내포(monad comprehensions)가 됩니다. 관심 있다면 찾아보세요.↩︎
모나드 클래스를 구현하는 것들—상당히 기묘한 것들까지—이 아주 많지만, 그에 대한 링크는 하스켈 타입 시그니처의 거대한 목록만 제공할 뿐이며, 이해하려면 하스켈을 알아야 합니다. 게다가 실제 이름도 “and_then”이 아니지만, 그건 구현 세부사항일 뿐입니다.↩︎
음, 요즘은 OCaml 같은 ML 계열 언어들도 이와 비슷한 시도를 합니다.↩︎