하스켈 입문자들한테 기본 모나드 중 뭐가 가장 이해안되냐고 물어보면 십중팔구 Cont랑 IO 얘기할거다. 

Cont야 안쓰면 된다 쳐도 IO는 안 쓸 수가 없는데 이해가 안되니까 문제다.

모나드랑 do/for 문법에 대해서는 이 글에서 다뤘고 Cont 모나드는 이 글에서 다뤘다. 이번 글에서는 IO 모나드를 좀 더 자세히 다루어 보기로 한다.




(1) 상상도 못한 정체



모나드 튜토리얼들 보면 IO 모나드가 뭔지를 유독 애매하게 다루는 경우가 많은데 이유는 간단함. 그냥 가르쳐주는 애들도 이게 뭔지를 잘 모른다. 

모르니까 이악물고 활용얘기만 하는거.


나는 물론 IO〈T〉의 의미를 바로 선언하고 시작할거다.


타입의 값을 반환하는 인자 없는 I/O 프로시저


부작용을 가질 수 있는 명령형 언어들의 함수를 프로시저라고도 한다는 건 알거임. 그 프로시저 맞다.

I/O 빼고 말해도 됨. 'T타입의 값을 반환하는 인자 없는 프로시저.'

함수형 언어의 함수들은 순수해야 하는데 그럼 함수만으로는 프로그램을 만들 수 없으니까 따로 프로시저 타입을 만든거다.



우선 이게 모나드가 맞는지부터 확인해 보자. liftunitflat을 정의할 수 있는지만 보면 되겠지. 


언제나처럼 자바 비스무리한 의사 언어를 사용할거다. 

이 언어는 프로시저랑 함수 모두 정의할 수 있는데 프로시저는 다른 프로시저 내부에서만 부를 수 있으며 타입 앞에 _proc 접두사가 붙는다.

IO〈T〉_proc () → T를 감싸는 래퍼 타입이 된다.

편하게 람다로 정의할것임. T타입의 값 t를 넣었을 때 f(t)가 나오는 함수를 (T t) => f(t) 라고 쓰는 문법이다.



lift는 이렇게 정의하면 된다.



unit은 이렇게,




flat은 이렇게 정의하자.




워낙 쉬운 코드들이라 딱히 설명할 게 있을까 싶다.


다만 이렇게 IO 모나드의 기본 함수를 직접 구현하는 건 프로시저가 있는 언어에서나 가능한거고 아예 함수만 있는 언어에서는 안된다.

하스켈에 IO 모나드의 구현이 없는건 그런 이유 때문이라고 보면 되겠다.




(2) 불가능을 가능으로 만든 IO 모나드?



아마 대부분의 사람들이 모나드에 대해 궁금해하는 건 그걸 가지고 어떻게 I/O를 하는 프로그램을 순수하게 프로그래밍할 수 있느냐일 거다.

모나드가 대단해 봐야 함수 몇개 달린 타입 컨스트럭터일 뿐인데

어떻게 순수하지 않은 프로그램을 순수하게 만드는 형용모순같은 짓을 저지를 수 있는지를 다들 궁금해한다.


미스터리는 그걸 하는 하스켈의 main을 보면 바로 풀린다. 

다른 언어의 main은 함수지만 하스켈에서는 IO〈()〉타입의 값이다. (하스켈 표기로는 IO ())

_proc () → () 타입의 프로시저라는 거지. 



결국 하스켈도 프로시저를 쓰니까 순수하지 않은 거 아니냐는 의문이 들텐데 그렇지는 않다.

코드에서 프로시저들을 호출할 수가 없기 때문이다. 


대신 합성이 가능하다.

IO〈T〉T를 반환하는 인자 없는 프로시저니까 당연히 T에서 U로 가는 인자 있는 프로시저는 T → IO〈U〉타입의 함수로 나타낼 수 있는데

이전에 설명한 모나드의 성질에 의해 이런 것들을 합성할 수 있다. 



이른바 하스켈 프로그래밍이라는 건 기본 I/O 프로시저들을 레고마냥 조립해서 main을 얻는 과정일 뿐임.

언뜻 보면 명령형 코드처럼 보이는 다음 코드도 마찬가지다.




이걸 do 표기법 없이 쓰면 이렇게 된다.




getlineIO〈string〉 타입이고 putStrLnstring → IO〈()〉 타입이며 >>=는 모나드 M에 대해 M〈T〉T → M〈U〉 합성해주는 하스켈 연산자다. 

그냥 _proc () → string랑 _proc string → ()랑 합성해서 main 만드는 코드라는 걸 알 수 있다.



결론적으로 하스켈 코드는 순수한 게 맞다.

그런데 어떻게 그걸로 I/O를 하는 프로그램을 만들 수 있냐, 코드가 프로그램으로 컴파일되는 게 아니기 때문이다.


하스켈에서 main = 우변의 코드는 main의 본문이 아니다. main을 조립하는 코드고 실행시간에 실행될 필요조차 없음. 

다른 데 있는 코드도 마찬가지다. 모두 컴파일 시간에 실행되고 그 결과로 조립된 main만 프로그램으로 컴파일돼서 실행시간에 실행되는 것이다.




(3) 결론



IO 모나드는 어렵지 않다. 내가 보기엔 State 모나드 같은것보다도 이게 더 간단함.

근데 왜 어렵다고 소문이 났냐, 설명을 이상하게 해서임.



IO 모나드에 대한 설명 태반이 IO〈T〉World → (T, World)로 설명하던데 뭐하는 짓인지 모르겠다. 이거 말이 되기는 함?


상태 프로세스야 같은 초기상태에 대해 같은 결과값이랑 결과 상태를 주면 다 똑같다고 할 수 있으니까 S → (T, S) 로 나타낼 수 있지만

I/O 프로세스는 중간에 뭘 하느냐에 따라 다 다른 프로세스인데...


World에 사용자 기억까지 포함한다고 치면 관점에 따라 될 것 같기도 하다.

근데 IO 모나드를 모르는 사람은 무조건 오개념 생긴다. 

맞는지도 의심스럽고 배우면 무조건 오개념 생기는 설명을 왜 하는 거지? 도대체 왜?



하스켈의 IO 액션과 본질이 같은 개념 중 대다수의 프로그래머들에게 가장 친숙한 개념은 명령형 언어의 프로시저다.

모든 프로그래머들이 이거에 너무나 친숙한 나머지 그냥 IO 액션을 프로시저로 설명하기만 하면 그것의 개념에서 활용까지 모든걸 일사천리로 이해하는데

다들 세계의 상태 모나드 이딴거로 외도함. 보면서 빡이 쳐 안쳐?


구현이 필요하면 그냥 잠깐 프로시저 정의문이 있다고 치고 의사 코드로 설명하거나 프로시저가 있는 언어로 설명하면 되는데

그렇게 하면 IO 모나드가 왜 모나드가 되는지, 왜 하스켈은 IO 모나드 구현을 보여주지 않는지를 다 답해줄 수 있는데 그걸 나빼고 아무도 안한다.



나도 한때는 세계의 상태 모나드 헛소리의 피해자였음. 그때 엉터리 설명때문에 오개념 생겨서 고생했던거 생각하면 아오




죽창이 필요하다...



하여간 IO 타입은 그냥 프로시저 타입이라고 이해하면 쉽다.

물론 모나드 규칙을 만족하느냐를 따지기 시작하면 좀 복잡해지는데 왜 따져 이걸.


굳이 따지자면 프로시저 두개가 실행속도까지 완전히 같아야 같은 거라면 이 글의 구현대로는 안 된다.

가령 iot가 즉시값 t를 반환하는 프로세스면 lift(f)(iot)도 즉시값 f(t)를 반환하도록 lift를 최적화해야 되는데 이걸 할려면 리스프 정도 되는 언어가 필요함. 


...그냥 되게 할 수 있다는 것만 알아두도록 하자.




모나드 관련글은 이게 마지막임. 지긋지긋하기도 하고 내가 아는게 좁아도 모나드 원툴까지는 아니다.

github의 강의 다 완성되면 짧게 홍보정도나 할거임.


애초에 내가 자신있는건 프로그래밍의 특정 주제라기보다는 나만의 사고법, 접근법, 아이디어 같은 것들이고

모나드 강의도 그냥 다수가 안된다고 하지만 내가 보기엔 될 것 같아 보이는 주제에서 내 접근법이 얼마나 통하는지 테스트해보려고 뛰어든 것뿐인데

병신이 이게 좀 될 각 보이니깐 또 그 접근법 자체에 심취해서 병신짓함.


나도 중요한 걸 잊고 있었던 것 같은데

일단은 자숙의 시간을 좀 가져야겠다. 언젠가 다른 쓸모없지만 재미있는거 다루는 글로 돌아오겠음.


다뤄줬으면 하는 주제가 있으면 제안해도 됨. 하스켈 말고 다른거로 부탁한다.