‘IO 모나드’라는 표현이 하스켈 학습과 실천에 끼치는 해악을 지적하며, IO는 모나드가 아니라 IO 타입의 IO 액션으로 이해해야 한다고 주장하는 글. 오해를 부르는 답변과 잘못된 교육 관행을 비판하고, 언제 ‘X 모나드’라 부르는 것이 적절한지 예시와 함께 정리한다.
“~는 해롭다(considered harmful)” 류의 글 전통에 따라, 이 글의 제목은 의도적으로 오해를 유발하고 논란을 부추기도록 지어진 것입니다 — 적어도 여러분의 관심을 끌기 위해서요. 그러니 이 글 전반의 과장은 있는 그대로, 과장으로만 받아들여 주세요 :) 전통을 이어 가능한 한 많은 인용구와 한 줄 문구를 남겨, 맥락을 벗어나 끔찍하게 왜곡되기 쉬운 상태로 만들어 보겠습니다.
아무튼, 내가 말하고자 하는 건 “IO 모나드”라는 것이 피해야 할 무엇이라는 뜻이 아닙니다. 사실 그 안에 내가 좋아하는 점이 아주 많습니다. 내가 말하고자 하는 건 “IO 모나드(IO Monad)”라는 표현 자체… 이건 이제 그만해야 한다는 겁니다. 쓰임새가 아예 없진 않지만, 쓰이는 경우의 99.9%는 부적절하고, 그 해악이 큽니다. 그러니 이 말도 안 되는 일을 이제 그만 끝냅시다, 네?
여기서 분명히 말하겠습니다:
“IO 모나드”라는 표현은 해롭다. 제발 쓰지 말자.12
대부분의 상황에서, _IO 타입_의 IO 액션3이라고 말하는 편이 더 유용하고 더 정확합니다.
이는 아마 하스켈과 하스켈 커뮤니티에서 교육, 실천, 대중 인식, 그리고 고양이들에 이르기까지 관련하여 가장 해롭고 파괴적인 단 하나의 것일지도 모릅니다. 농담 아님. 누군가 이 표현을 말할 때마다 전 세계 모두가 손해를 봅니다. 그 자체로도 문제지만, 하스켈 문제의 90%(±80%)의 근본 원인이기도 합니다.
누군가 와서 이렇게 묻는다고 해봅시다. “하스켈은 문자열 출력 같은 일을 어떻게 하나요?”
답은 이렇습니다: 절대로 IO 모나드로 하는 게 아닙니다.
이건 하스켈을 처음 접하는 사람이 할 수 있는 가장 단순한 질문 중 하나입니다. 틀린 답은 여럿 있겠지만, “IO 모나드”는 가능한 답 중에서도 가장 틀린 축에 속합니다.
한 가지, 하스켈의 가장 아름다운 점은 IO 액션이 리스트나 정수, 불리언처럼 모두 일급의 평범한 데이터 객체라는 것입니다.
정답은 IO 액션 — 즉 IO
타입의 무엇 — 을 사용한다는 겁니다.
_IO 타입_의 _IO 액션_을 사용하세요
ghci> :t putStrLn "hello world"
putStrLn "hello world" :: IO ()
문자열을 출력하는 데 모나드와 관련된 건 하나도 없습니다. putStrLn "hello world"
가 모나딕하다고 말하는 건 [1,2,3]
이 모나딕하다고 말하는 것만큼이나 우스꽝스럽습니다.
답을 “IO 모나드”라고 하는 건, 모나드인 점이 중요하다는 뉘앙스를 담습니다. 그렇지 않습니다.
하스켈의 IO는 모나드와 아무 상관이 없습니다.
물론, 진실은 조금 더 복잡합니다.
IO의 모나딕 인터페이스는 그 역사와 밀접하게 얽혀 있습니다. 하스켈이 채택한 IO 모델이 선택된 _이유_는 그 모델이 _모나딕 인터페이스를 제공하기 때문_이라고도 할 수 있습니다. 그 IO 모델이 모나딕 인터페이스를 인정한다는 사실이 하스켈의 _대표 IO 모델_로 채택되는 데 큰 요인이었습니다.
그러니, 모나드는 하스켈 IO의 설계 결정과 “무관하진 않습니다”. 하지만 하스켈이 IO를 하려면 IO가 모나딕 인터페이스를 가져야만 하는 건 아닙니다. 다만 그건 한 줄 요약이나 자극적인 문구로 만들기엔 덜 그럴싸하죠?
이 중요한 보충 설명에 대해 Chris Allen과 Kevin Hammond에게 특별히 감사드립니다.
모나드, 심지어 모나딕 인터페이스 전체를 하스켈에서 걷어내더라도, 하스켈은 여전히 같은 IO 타입으로 IO를 할 수 있습니다.
하스켈이 IO를 다룰 수 있는 능력은, Bool
이 불리언을, Integer
가 정수를 표현하듯, IO 액션을 표현하는 평범한 데이터 타입이 있다는 사실에서 옵니다.
“IO 모나드”라고 말하는 건 당신이 할 수 있는 말 중에 정말로 가장 오해를 부르는 말입니다. 하스켈의 IO는 모나드와 아무 상관이 없습니다.4
어쩌다 이런 생각이 이렇게 널리 퍼졌을까요? 나도 모르겠습니다! 하지만 어딘가에서 이 생각이 싹텄고, 지금껏 끈질기게 남아 있네요. 이 오해에 무엇이든 보태서 이 위험한 신화를 더 퍼뜨리지 말아 주세요.
“IO 모나드”라고 하는 건 끔찍하고 잘못된 교육법입니다. 왜냐면 하스켈을 처음 접한 사람이 “문자열을 출력하거나 IO 액션을 하는 건 ‘IO 모나드’를 쓰는 거다”라는 말을 들으면 자연스럽게 이렇게 묻습니다. “모나드가 뭔가요?”
그 질문은 IO를 하는 데 완전히 무관할 뿐만 아니라, 역사적으로도 큰 혼란을 낳아 왔습니다. 나는 그걸 하스켈 학습에서 떠나는 “최악의 사이드퀘스트” 중 하나라고 생각합니다. 모나드가 무엇인지에 대한 직관을 구하는 건 (초반의) 실용적인 하스켈 학습에는 무익할 뿐만 아니라, 틀린 답, 혼란스럽고 모순된 답, 머리 아픔으로 이어지기 쉽습니다. 나는 하스켈을 알기도 전에 악명 높은 “IO 모나드” 이야기를 먼저 들었습니다. “모나드는 미친 듯이 이해하기 어려운 주제지만, 그걸 이해하면 하스켈이 놀라워진다”는 말을 읽었죠. 하스켈은 모나드를 도입하기도 전에 이미 하스켈이고, 유용합니다. 그런데 그런 말은 마치 하스켈이나 IO를 이해하는 데 모나드를 이해하는 게 중요하다는 뉘앙스를 줍니다.
그건 그냥 사실이 아닙니다. “모나드를 이해”하고 싶다면(그게 무엇을 의미하든), 하세요. 다만 그게 하스켈의 IO를 이해하는 데 단 1도 도움이 되리라 생각하진 마세요.
“IO 모나드”라고 말하는 건 모나드를 이해하는 것이 IO를 이해하기 위한 선행 조건이라는 뜻을 내포하거나, 최소한 하스켈의 IO가 본질적으로 모나드에 얽혀 있다는 인상을 줍니다. 둘 다 사실이 아닙니다.
또 자주 잘못 답하는 질문이 있습니다. “순수한 언어인 하스켈은 불순한 부작용을 어떻게 다루나요?”
역시, “IO 모나드”만 빼고 뭐든지가 답입니다. 내가 가장 오해를 부르고, 틀리고, 위험하고, 끔찍한 답을 꼽자면 이게 바로 1위일 겁니다. 이걸 가능케 하는 건 _IO 타입 그 자체_입니다. 모나딕 인터페이스가 아닙니다.
여기에 “모나드”라는 개념을 끌어들이는 건 혼란만 더할 뿐입니다. 정말로 아무것도 기여하지 않거든요. 그런데도 왜 “하스켈은 모나드로 IO와 불순성을 다룬다”고 답하는 걸 자주 보게 될까요? 분명 한 번은 들어보셨을 겁니다. 하지만 이건 100% 틀렸습니다. 모나드는 실제로 아무 상관이 없습니다. 과장도 아닙니다.
오히려 하스켈의 학습 장벽을 높일 뿐입니다. IO처럼 단순한 것도 이해하려면 _범주론_이 필요하다면 뭔가 크게 잘못된 거죠. (다행히 그럴 필요는 없습니다) 이런 말들은 하스켈을 사람들로 하여금 그저 똑똑해진 기분을 느끼기 위해 배우는 학술 언어로 보이게 하는 인식만 더합니다. 하스켈은 이미 큰 홍보(PR) 문제를 안고 있습니다. 여기에 이런 말로 기름을 붓지 말아 주세요.
더 나아가, 하스켈을 처음 배우는 사람이 이렇게 묻는다고 합시다. “숫자들의 시퀀스를 저장할 수 있나요?”
좋은 답: “네, 리스트로요!” 혹은 리스트 타입을 쓰세요.
나쁜 답: “네, 리스트 모나드로요!”
이제 [1, 2, 3]
같은 단순한 일을 하고 싶은 사람이 하스켈에서 [1, 2, 3]
같은 것이 어떤 식으로든 모나드에 본질적으로 묶여 있다고 생각하게 됩니다.
하지만 [1,2,3]
같은 리스트를 갖는 건 모나드와 아무 상관이 없습니다. 모든 리스트를 “리스트 모나드”라고 부르거나, 리스트가 유용한 모든 상황을 “당신은 리스트 모나드를 원한다”라고 부르는 건 오해를 낳고, 거짓이며, 혼란만 키웁니다.
1부터 100까지의 모든 짝수를 찾아야 합니다.
정답: 리스트를 사용하고, 1부터 100까지의 리스트에 filter even
을 하세요.
오답: 리스트 모나드를 사용하고, 1부터 100까지의 리스트에 filter even
을 하세요.
더 틀렸지만 사실 애초에 더 틀릴 여지도 별로 없음: 리스트 모노이드를 사용하고, 1부터 100까지의 리스트에 filter even
을 하세요.
도대체 왜 그렇게 하죠?
그게 무슨 도움이 되죠?
누구한테 언제 도움이 됐나요?
정말, 왜요?
왜 사람들은 IO 모나드라고 하나요?
왜 처음에 그런 말을 하기 시작했죠?
왜 이 세상은 아무 의미가 없죠?
제발, 제발, “IO 모나드”라고 말하는 걸 그만하세요.
“State 모나드”, “Writer 모나드”, “Reader 모나드”도 남용되면 똑같이 나쁩니다. 당신도 알죠. 반성합시다.
“(무언가) 모나드”라는 표현을 쓸 만한 좋은 때는, 그 모나드 인스턴스나 그 모나딕 인터페이스 자체를 가리킬 때입니다.
나는 “IO 모나드”의 “모나드”라는 부분에 문제를 제기하지만, 가끔은 “IO” 부분조차 의문입니다. 그렇습니다, 하스켈은 IO 타입을 통해 IO를 할 수 있지만, IO 타입을 직접 다루지 않고도 IO와 상호작용하는 효과적인 코드를 작성할 수 있습니다. 심지어 IO를 GHC나 당신이 쓰는 하스켈 컴파일러가 이해하는 멋진 “중간 데이터 구조” 정도로 생각할 수도 있죠. 라이브러리나 DSL 등으로 IO 기반 코드를 작성하고, 그걸 IO로 변환하는 함수만 있으면 됩니다. 이미 현실 세계의 많은 하스켈 코드는 “IO를 한다”고 해도 IO 타입 자체를 직접 다루지 않습니다.
지금쯤 혼란스러울 분들을 위해, “X 모나드”라고 부르는 게 적절한 경우도 있습니다. 모나딕 인터페이스를 실제로 활용할 때입니다. 마치 배열을 이터레이터 인터페이스로 사용할 때 그걸 이터레이터라고 부르는 것과 같습니다. 예시는 다음과 같습니다.
putStrLn
을 쓰세요.Int
를 반환하는 IO 액션을 (짝수이면) Bool
을 반환하는 IO 액션으로 바꿔야 한다”: 아니오; fmap
으로 Int -> Bool
을 IO Int
위에 “맵”하세요.mapM_
, (>>)
같은 모나딕 인터페이스를 사용할 수 있습니다. 그러나 이건 약간 오버킬이라 남용이라고 부르겠습니다; (*>)
와 traverse_
로도 충분합니다.IO
타입으로 하는 건 이 작업에 악명이 높게 나쁩니다. 쓰지 마세요; 물론, 그 모나드 인스턴스도 쓰지 마세요.수정: 많은 분들이 내가 IO
에서 (>>=)
를 쓰거나 do 블록을 쓰거나 “바인딩” 표현을 하는 걸 설명할 때, 그것들을 “모나딕”이라고 부르지 말자고 주장한다고 받아들이는 듯합니다. 나는 그걸 주장하는 건 아닙니다. 만약 당신이 구체적으로 모나딕한 것들을 말하고 있다면, 그것들이 모나딕하다고 말하길 권합니다. 사람들에게 무언가를 숨기는 건 누구에게도 도움이 되지 않고 오히려 혼란만 키우며, 중요한 연결을 배우는 데 방해가 될 수도 있다고 봅니다.
그러나 내가 _반대_하는 건, 분명 모나딕하지 않은 것들을 “모나드”라고 부르는 것입니다. 문자열을 출력한다든지, 서로 독립적인 액션을 나열해 실행한다든지 하는 것 말이죠.
요컨대: IO의 모나딕 성질을 말할 때는 Monad를 쓰세요; 그게 아닐 때는 쓰지 마세요. 간단하죠?