1. 우선 모나드부터 알아보자



하스켈의 do도 스칼라의 for도 기본적으로 모나드 기반의 문법이기 때문에 이해하려면 모나드를 좀 알아야 함.

문제는 그 모나드 아는게 어려움.

최근에 글 하나 올라온 거 있긴 한데 이거 보고 이해하라고 떠넘기고 싶지만 솔직히 이 설명도 마음에 안 들어서 그냥 내가 다시 설명하겠음.




(1) 뭐하는 개념임?



대개 모나드 가르치는 자료들 보면 서론과 동기에 대한 설명만 엄청나게 길고 결국 그래서 모나드가 뭔지에 관해서는 부실함.

설명을 끝까지 읽어도 결국 모나드가 뭔지는 고사하고 그게 어디에 속하는 건지도 모름.

이새끼가 타입인지 클래스인지 제네릭인지 템플릿인지 디자인 패턴인지조차 끝까지 불분명한데 그게 뭔지가 이해가 될 턱이 있나?


나는 그런거 아주 싫어하니까 단도직입적으로 모나드가 뭔지부터 설명하도록 하겠다.


모나드는 기본적으로 타입 컨스트럭터(type constructor)의 하위 분류임.

타입 컨스트럭터가 뭐냐면 그냥 타입 넣으면 타입 나오는 것들을 다 통칭해서 일컫는 말이라고 생각하면 됨.


예를 들면 자바의 타입 제네릭이나 C++의 타입 템플릿 같은 것들이 있겠다.

그러니까 타입 T가 주어지면 M〈T〉타입을 만들어 주는 M같은 것들. 다들 써봤을 거임.


그럼 이것들 중에 어떤 것들이 모나드가 되느냐, 다음 조건을 만족시키는 것들임.



첫째, 임의의 타입 간의 함수를 해당 모나드 타입 간의 함수로 바꾸는 변환이 있어야 함. 이름은 보통 liftmap.

가령 이런 함수 제네릭 같은 게 있으면 된다는 거다.


lift〈T, U〉 : (T→U) → (M〈T〉→M〈U〉)


꼭 위 타입으로 제공되는 건 아니고 언어에 따라 (M〈T〉, T→U) → M〈U〉등의 동치인 함수 타입으로 제공되는 경우가 더 많긴 한데 굳이 구분하지 않도록 하겠다.

(M〈T〉, T→U) → M〈U〉에서 인수 두개 순서만 바꾸고 커링하면 위 함수가 됨. 둘이 동치다.



둘째, 임의의 타입 T에 대해 TM〈T〉로 바꾸는 '자연스러운' 형 변환 함수가 존재해야 함. 이름은 unit이나 pure등으로 다양한데 하스켈에서는 몰지각한 이유로 return이라고 함.


unit〈T〉 : T → M〈T〉



셋째, 임의의 타입 T에 대해 M〈M〈T〉〉M〈T〉로 바꾸는 '자연스러운' 형 변환 함수가 존재해야 함. 이름은 보통 join이나 flat.


flat〈T〉 : M〈M〈T〉〉 → M〈T〉


그러니까 함수를 확장할 수 있는 타입 컨스트럭터 가운데 TM〈T〉로 볼 수 있고 M〈M〈T〉〉M〈T〉로 볼 수 있는 것들이 모나드라고 생각하면 되겠다. 별 거 아니지?

물론 이 세 함수가 만족시켜야 하는 조건들이 또 있는데 이 부분은 과감히 생략하고 넘어가겠음.




(2) 이거가지고 왜 발광함?



모나드가 함수형 언어에서 주목받은 건 모나드 자체의 특성 때문이 아니고 모나드 타입을 반환하는 함수들의 특성 때문임.

TM〈U〉타입의수들이 단순히 T에서 M〈U〉로 가는 함수가 아니라,

T에서 U로 가는 함수의 어떤 기능적 확장 버전 같은 의미로 해석될 수 있음이 발견되었던 것. 구체적으로 어떻게 그렇게 되는지 사례와 함께 알아보도록 하자.



사례1 : Optional 모나드


자바에 Optional 이라는 타입 제네릭이 있음. T타입의 값일 수도 있고 아무것도 아닐 수도 있는 값의 타입이 Optional〈T〉.

자바 말고도 언어마다 이런 타입 컨스트럭터 하나씩 다 있는건 알것임.


당연히 모나드다.

T에서 U로 가는 함수가 있으면 Optional〈T〉에서 Optional〈U〉로 가는 함수도 만들 수 있고

TOptional〈T〉로 변환할 수 있고 Optional〈Optional〈T〉〉Optional〈T〉로 변환할 수 있으니까.


이제 Optional을 반환하는 함수를 생각해 보자.


f : T → Optional〈U〉


물론 그냥 이걸 T에서 Optional〈U〉로 가는 함수로 볼 수도 있지만, T에서 U로 가는 함수는 함수인데 결과값이 없을 수도 있는 함수로 보는 것도 가능함.

실제로도 그런 함수 만들 때 주로 Optional이 쓰이기도 하고.



사례2 : List 모나드


List라는 타입 제네릭이 있음. T타입의 값들을 요소로 하는 리스트의 타입이 List〈T〉.

당연히 모나드임. T에서 U로 가는 함수가 있으면 List〈T〉에서 List〈U〉로 가는 함수도 만들 수 있고 (모든 요소에 주어진 함수 거는 함수 만들면 됨)

TList〈T〉로 변환할 수 있고 List〈List〈T〉〉List〈T〉로 변환 가능함. 그냥 요소 다 풀어 합치면 되니까.


이제 List를 반환하는 함수를 생각해 보자.


f : T → List〈U〉


T에서 U로 가는 함수는 함수인데 결과값이 여러 개 있을 수도 있는 함수로 보는 것이 가능함. 실제로도 그런 함수 만들 때 List가 잘 쓰임.



중요한 건 이게 한두 사례만 그런 것이 아니고 모든 모나드가 다 그렇다는 거다.

더 중요한 건 그 역도 성립한다는 거.

상상할 수 있는 T에서 U로 가는 함수의 모든 기능적 확장 버전은 다 어떤 모나드 M에 대해 T → M〈U〉타입의 함수로 나타낼 수 있었음.

특히 함수형 언어 덕후들을 뿅가게 한 것이 있었으니 바로 IO 모나드였다.



사례3 : IO 모나드


IO〈T〉는 'T타입의 값을 주는 실행 가능한 IO 프로세스' 의 타입임. 그냥 T타입 반환하는 인자 없는 프로시저의 타입이라고 보면 된다.

부작용 있는 언어라면 이런 건 그냥 ( )→T 타입의 함수로 구현하면 되겠지.

근데 함수형 언어에서 부작용 있는 함수 못 만들잖아? 그래서 그런 걸 다루기 위해 함수랑 구분되는 별도의 프로시저 타입을 만든 거라고 보면 아주 정확함.


IO도 당연히 모나드임. 우선 T에서 U로 가는 함수가 있으면 IO〈T〉IO〈U〉로 바꾸는 함수도 만들 수 있음. IO〈T〉의 결과값에 그 함수를 걸면 IO〈U〉가 되니까.

즉시값은 아무 것도 하지 않고 값을 반환하는 IO 프로세스라고 볼 수 있으니 TIO〈T〉로 변환할 수 있겠지.

'T타입의 값을 주는 프로세스를 주는 프로세스' 도 당연히 'T타입의 값을 주는 프로세스' 로 변환할 수 있음. 결국 IO〈IO〈T〉〉IO〈T〉로 변환 가능함.


그럼 이제 이 IO모나드를 반환하는 함수가 뭐가 될지 생각해 보자.


f : T → IO〈U〉


T에서 U로 가는 함수는 함수인데 IO 부작용을 가진 함수. 어라?



프로그램은 결국 IO 부작용을 가져야 의미 있는 프로그램이 될 수 있음.

그래서 순수 함수들만으로는 프로그래밍을 할 수 없다는 게 지금껏 함수형 언어들이 풀어야 할 숙제였는데 그 문제가 바로 풀려버린거임.

여기서 함수형 덕후들 다 뿅가죽고 지금까지 모나드 가지고 그 난리를 치게 된거다.



모나드 기반의 확장된 함수들은 물론 합성도 정의할 수 있음. T→M〈U〉U→M〈V〉를 가지고 T→M〈V〉를 만들 수 있다는 얘기다.

이것은 flat과 lift로부터 다음 함수를 정의할 수 있기 때문임.


flatlift〈T, U〉 : (T→M〈U〉) → (M〈T〉→M〈U〉)


이것도 언어에 따라 (M〈T〉, T→M〈U〉) → M〈U〉 같은 동치인 타입의 함수가 대신 쓰이는 경우가 많지만

어차피 동치고 사용법도 거기서 거기니까 구분하지 않도록 하겠다.

명칭도 보통 bindflatmap을 쓰는데 그냥 내가 앞에 있는 lift랑 깔맞추려고 flatlift라는 이름을 썼음. 알아서 알아듣자.



flatlift 정의하는 방법은 간단함. T→M〈U〉타입의 주어진 함수가 있을 텐데 그걸 lift하면 M〈T〉→M〈M〈U〉〉타입의 함수가 나옴.

그걸 flat이랑 합성한 M〈T〉→M〈U〉타입의 함수를 반환하면 됨. 이런 거 굳이 코드 예제 필요하지 않지?


이거 있으면 당연히 확장된 함수들을 합성할 수 있음.

가령 T→M〈U〉타입의 함수랑 U→M〈V〉타입의 함수를 합성하려면 뒤의 것을 flatlift해서 M〈U〉→M〈V〉로 만든 다음에 앞의 거랑 합성하면 됨.


이렇게 합성을 정의하면 M이 무슨 모나드든 간에 신비하게도 항상 그 모나드에 기반을 두는 확장된 함수의 의미상 가장 자연스러운 합성이 나온다.

이유는 설명 가능하나 이 글에 담기에는 여백이 부족하므로 생략하도록 하겠다.



사실 모나드를 지원하는 프로그래밍 언어 대부분은 flat이 아니라 flatlift를 더 근본적인 함수로 지원함.


앞에서 flatlift가지고 flatlift정의하는 거 얘기했는데 거꾸로 flatlift로부터 flat을 정의하는 것도 가능하기 때문임.

인자를 그대로 반환하는 M〈T〉→M〈T〉 타입의 함수에 flatlift걸면 바로 flat나옴.


실제 하스켈을 비롯한 함수형 프로그래밍 언어들이 제공하는 모나드를 보면 십중팔구 liftunit, flatlift를 가진 타입 컨스트럭터 형태다.



문제는 flatlift는 모나드 반환하는 함수들을 이해하기에는 좋을지 몰라도 모나드 자체를 이해하는 데는 쥐뿔도 도움이 안됨.


하도 애들이 그걸 가지고 질문하니까 그래서 만들어진 게 모나드가 무슨 '상자' 니 '부리또' 니 하는 지나가는 개도 거들떠보지 않을 비유들인데

세상천지에 아무런 쓸데도 없는 무가치한 비유들이라는 것을 논리적 합리적으로 증명할 수 있으며 이는 역사적으로도 증명됨.


모나드가 뭔지 감 잡고 싶으면 그냥 내가 1-(1)에 써놓은 설명 읽어라. 그게 직빵이다.





2. 그래서 do 왜 씀?



제목에서 하스켈의 do랑 스칼라의 for설명한다고 했는데 이 둘은 이름만 다르고 똑같은 거라고 보면 된다. 하스켈의 do를 기준으로 설명하겠음.




(1) 다변수 함수가 문제다



모나드 도입하고 처음에는 아주 좋았음.

언어에 대충 확장된 함수를 자동으로 flatlift해서 M〈T〉타입 값으로 호출하는 연산자로 [ ] 같은거 하나만 넣어 줘도 쓰기가 아주 편리하다는 걸 알 수 있다.

가령 vM〈T〉타입의 값이고 fT→M〈U〉타입의 확장된 함수고 gU→M〈V〉타입의 확장된 함수면 이런 코드를 쓸 수 있겠지.


g[f[v]]


그러니까 괄호를 ( ) 대신 [ ]를 쓰는 것만으로도 확장된 값과 함수들을 그냥 값과 함수들처럼 이용할 수 있다는 것.



물론 위에건 그냥 예를 든 거고 하스켈에서는 [ ] 대신 >>=라는 이항 연산자를 사용함. 위 코드는 하스켈에서는 이렇게 됨.


v >>= f >>= g


인수가 함수보다 앞에 오는게 특이하긴 해도 (이건 장단점이 있는듯) 간단한 건 마찬가지라 익숙해지면 쓸만함.



문제는 다변수 함수들. 다변수 함수들도 모나드를 이용해 (T, U)→M〈V〉같은 확장된 함수로 정의하고 일반 함수처럼 쓰고 싶은데 이게 그렇게 만만치가 않음.

(T, U)→M〈V〉X→M〈T〉, Y→M〈U〉와 합성할 수 있어야 하는데 이게 쉽지가 않거든.


대부분 함수형 언어에 다변수 함수가 없기는 하지만 대신 커링된 함수들이 있고 얘네도 결국 똑같은 문제를 야기함.

T→U→M〈V〉X→M〈T〉, Y→M〈U〉랑 합성할 수 있어야 한다.

진짜 다변수 함수든 커링된 다변수 함수든 프로그래밍에서 무진장 많이 쓰이기 때문에 이걸 못 하면 모나드는 그냥 아무짝에도 쓸데가 없는 장난감임. 문제는 이걸 어떻게 하냐는 거.




(2) 간단한(?) 해결책



지금 문제는 (T, U)→M〈V〉가 요구하는 인수의 타입이 X→M〈T〉, Y→M〈U〉가 반환하는 값들의 타입과 달라서 이들을 연결할 수 없다는 거다.

(T, U)→M〈V〉를 어떻게든 M〈T〉M〈U〉로 호출할 수만 있다면 문제는 해결됨.

(T, U)→M〈V〉타입의 함수 f가 주어져 있다고 치자. 이걸 어떻게 하면 M〈T〉타입의 값 mtM〈U〉타입의 값 mu로 호출해서 M〈V〉타입의 값을 얻을 수 있을까?



결론부터 보여드리도록 하겠음. 대충 이런 식으로 하면 됨. (자바 의사 코드임.)



이게 뭣이여 쉬불;


멘탈나가는거 당연함. 그래도 참고 읽어주길 바람. 안쪽에서부터 해석하면 이 코드 생각보다는 안 어려움.


우선 가운데에 있는 f(t, u)라는 식에 주목할 필요가 있다. 이 식은 tu라는 미지수를 포함하고 있는 M〈V〉타입의 식임.

만약에 t가 이미 주어져 있다고 치면 이 식을 U타입의 미지수 u하나에만 의존하는 M〈V〉타입의 식, 즉 U→M〈V〉타입의 함수로 볼 수 있겠지.

거기에 flatlift를 걸고 mu로 호출하면 M〈V〉타입의 값이 얻어짐. 가운데 세 줄은 바로 이걸 하는 코드인 것이다.


그리고 그 코드는 다시 T타입의 t에 의존하는 M〈V〉타입의 식으로 볼 수 있음.

이걸 다시 T→M〈V〉타입의 함수로 만든 후 flatlift하고 mt로 호출하면 최종적인 M〈V〉타입의 결과를 얻는것임.



논리가 꽤 복잡하다고 느낄 수 있는데 사실 전혀 그렇지 않음. 아래의 이중적분 식이랑 똑같다. (정적분 정도는 알고있기를 바람. 모르면 그냥 넘어가.)



이 식이 어떻게 계산되는가를 보면 우선 안쪽에 있는 x^yx만의 함수로 보고 적분한 다음 그 결과를 다시 y로 적분함.

앞에서 우리가 flatlift가지고 했던 짓이 바로 이거 아니냐?

그냥 우리가 일변수 함수에다가 하는 짓을 다변수로 확장할 때 늘 하는 게 이거임.



결정적으로 위와 같은 간단한 한 줄짜리 다중적분 식도 코드로 표현하는 순간 몹시 아름더러워진다.



이게 뭣이여 쉬불;


그렇다. 좆같음은 모나드 자체에 있었던 것이 아니고 이런 논리를 직관적으로 나타내기에 부적합한 람다식 자체의 근본적인 한계에 있었던 것이다.

(자바 문법이 좆같은거 아니냐고 할 수 있는데 하스켈로 써도 똑같이 좆같음. 해봐라.)


그렇다면 차라리 아예 이런 논리를 더 직관적으로 나타낼 수 있는 대안적인 표기법을 언어에 추가하면 어떨까?




(3) do를 쓰는 이유



이쯤이면 do 표기법이 뭐고 그걸 왜 쓰는지 감이 왔을 것임. 비유하자면 마치 적분 함수 대신 쓰라고 만든 이런 integra 표기법이랑 같다.



물론 이건 do가 어떤 느낌인지 설명하려고 내가 만든 거고 실제로 지원하는 언어는 없음.


여기서 y <- [0, 1] 는 그 줄 밑에 있는 integra 블럭 안의 식 전체를 y의 함수로 보고 0에서 1까지 정적분하라는 의미로 해석되는데,

do 표기법이 해석되는 방식도 이거랑 똑같다고 보면 된다.




t <- mt는 그 줄 밑에 있는 do 블럭 안의 식 전체를 t의 함수로 보고 flatlift한 다음 mt로 호출하라는 얘기다. u <- mu도 마찬가지고.

결국 (2)의 중첩 flatlift 식과 정확히 같은 식이라는 걸 알 수 있지.


가끔 뭔 번역 규칙 가지고 와서 뭐가 뭘 축약한 표기라면서 그거로 바꿔 놓고 이해하라고 하는 애들 있는데 하수거나 가르칠 생각 없는 애니까 무시해라.

그냥 써져있는 형태 그대로 놓고 내 말대로 이해하는 게 훨씬 쉬움. 그러라고 만든 표기법임.



do 표기법은 모나드 프로그래밍의 환경을 완전히 바꿔놓았다.

모나드가 확장된 함수를 합성할 수 있게 해 주는 도구를 넘어서 아예 연산 자체를 자동으로 확장해 주는 도구에 가깝게 됨.


이게 무슨 소리인지 다음 예제를 보도록 하자.



x*x는 본래 int 타입의 x에나 의미가 있는 식임. 그런데 do 블록이 그 식을 List〈int〉타입의 x로 이용할 수 있게 해 주고 있음.


물론 이 식은 정확히 말하면 x*xflatlift해서 그 flatlift한 걸 리스트로 호출하는 식일 뿐이다.

하지만 이런 코드를 쓰면 쓸수록 아 그냥 이 do라는건 T타입에 대한 식을 List〈T〉타입 가지고도 쓸 수 있게 해 주는 환경 구문이구나 하는 느낌으로 쓰게 됨.



당연히 이건 리스트 모나드만 가지고 되는 건 아님. Optional로도 비슷한 걸 할 수 있음.



a와 b는 Optional〈int〉타입의 값이었을 것임. (하스켈 타입으로는 Maybe int) 당연히 얘네를 더하려면 각각이 비어 있을 경우에 대한 복잡한 처리가 필요함.

그 처리를 모두 생략시키고 그냥 숫자마냥 둘을 더할 수 있는 것.


마치 do 블록 내부에 한해 Optional모나드 타입만을 위한 도메인 특화 언어가 하나 생긴 것 같은 느낌으로 코드를 쓸 수 있다는 거다.

하스켈 모나드의 생산성은 다른 무엇보다도 여기에 기인하는 것이라고 할 수 있겠다.






아마 이 글을 보는 독자들 중에는 지금까지 모나드가 뭔지 do 뭔지 계속 알쏭달쏭했다가 이 글 보고 겨우 감을 잡은 사람들도 꽤 있을 거라고 본다.


아닌가?


하스켈 공부한 사람들이 경험하는 대표적인 정신병 중 하나가 자기가 모나드를 쉽게 설명할 수 있다는 착각이긴 함.

나 또한 그런 정신병자가 아닐 거라고 완전히 확신은 못하겠다...

설명 이해 안되면 과감하게 악플달아주길 바람. 스스로가 병신임을 겸허히 받아들이고 시정토록 하겠음.



그래도 내 설명이 많이 특이하긴 할 것임. 모나드도 그렇고 do도 그렇고 나처럼 설명해주는 사람은 못 봤을거다.

특히 do가 그럼. 보통 이걸 뭔 문법 sugar라고만 가르치는데

최소한 그 괴상한 sugar가 왜 필요한지 납득가는 설명은 제시해줘야 할 거 아니냐. 적어도 나는 그걸 시도했다. 나 말고 이런거 하는자를 본적이 없음.


그런 의미에서 홍보할 게 하나 있다.




내 유튜브 채널이다.


현재 모나드 강의 시리즈 하나 올라가 있는 상태인데 특정 언어의 모나드에 국한된 강의가 아니라 프로그래밍의 모나드 자체를 설명해주는 강의고

제목 그대로 모나드의 모든 것을 다 다루는 강의라 범위가 엄청나게 넓음.

총 3장짜리인데 장마다 영상 한 10개정도 되고 아직 3장이 안 만들어졌는데도 전체 플레이시간이 4시간 가까이 됨.


열심히 만들었긴 한데 어떻게 홍보해야 할지 모르겠는 그런 상황이다. 만약 이 글의 내 설명 스타일이 좋다면 이 강의도 좋아할거라고 생각함.

모나드뿐 아니라 카테고리 이론에서 기초도 아니고 거의 중간급 되는 개념까지 완벽하게 개념잡아준다.


정보글쓰고 덤으로 홍보하는 것 정도는 괜찮지?