Home » IO모나드와 부수효과

IO모나드와 부수효과

하스켈에서 문자열을 화면에 출력해주는 putStrLn 함수를 생각해봅니다. 이 함수의 타입은 String -> IO ()로 문자열을 인자로 받고 빈 모나드를 리턴합니다. 무언가를 출력하는 일을 수행하니, 반환할 값에는 의미가 없다는 뜻입니다. 반환하는 값이 비어있으니, IO 액션의 연쇄에 있어서는 어떤 종착점이 될 수 있다고 봐도 됩니다. 말하자면 이 액션의 이후에는 어떤 처리를 하는 것이 의미가 없는 셈이죠. 실제로 하스켈 프로그램의 진입점이 되는 main 함수 역시 타입이 IO () 여서, main 함수의 실행이 종료되면 프로그램이 끝나게 됩니다.

통상적인 프로그래밍 경험에 비추어볼 때 우리는 표준입력을 통해서 유입되는 데이터가 단순히 문자열이라 가정합니다. 하지만 엄밀히 말해서 실제 외부세계로부터 프로그램 내로 어떤 정보를 주입하는 행위는 사실 순수하지 않습니다. (입력이 같은데도 다른 결과를 만들 수 있습니다.) 받아들이 데이터가 문자열이지만, readFile이라든지 getLine과 같은 함수들은 모두 이러한 IO 모나드로 둘러 싸이게 됩니다. 하스켈 프로그램 관점에서 볼 때 ‘입력’은 어떤 부수효과를 남기는데, 입력 동작으로 받아들인 문자열 정보가 바로 발생한 부수효과가 됩니다.

어떤 숫자를 입력받아서 이를 두 배한 수를 출력하는 프로그램을 작성해봅시다. 이 프로그램은 다음과 같은 기능을 갖추어야 합니다.

  1. 표준입력으로부터 문자열을 받습니다.
  2. 문자열을 정수값으로 변환합니다.
  3. 정수를 두 배합니다.
  4. 정수를 문자열로 변환합니다.
  5. 문자열을 표준출력을 통해출력합니다.

이때, 1, 5의 기능은 부가효과를 수반하는 동작이며, 2, 3, 4는 견고하게 결합되는 순수한 함수들의 조합이 될 수 있습니다. 하스켈에서 모나드는 순수한 영역과 그렇지 않은 영역을 결합하는데 사용될 수 있습니다.

먼저 2, 3, 4의 과정을 보면 각각 read, (*2), show 함수이므로 이를 다음과 같이 하나의 함수로 연결할 수 있습니다.

process :: String -> String
process = show . (*2) . read

이제 1, 5번을 보겠습니다. 1에 해당하는 액션은 getLine(:: IO String)이며 5에 해당하는 액션은 putStrLn(:: String - > IO ())입니다. 타입을 보면 이 두 함수는 같은 IO 모나드를 다루기 때문에 바인드 연산으로 결합이 가능합니다. 말 그대로 입력받은 문자열을 그대로 출력하는 프로그램이 됩니다.

-- 입력받은 라인을 그대로 출력
getLine >>= putStrLn

getLine으로 받아들인 값 내부의 문자열에 process 함수를 맵핑할 수 있으며, 그 결과는 putStrLn 함수에 바인드하여 출력할 수 있습니다.

process <$> getLine >>= putStrLn

하스켈은 do 문법을 제공하여 모나드 함수의 연쇄를 마치 절차형 프로그램처럼 기술할 수 있게 합니다.

main = do
  str <- getLine
  let result = process str
  putStrLn result

do 블럭으로 표기한 부분은 바인드를 사용해서 번역하면 다음과 같이 쓰여집니다. 사실 바인드 연산과 익명함수로 연결된 하나의 수식인 셈입니다.

main = getLine >>= \str ->
     let result = process str
     in puStrLn result

전체적인 흐름을 보면 순수한 영역의 함수는, 비순수한 영역 속으로 들어갈 수 있습니다. 함수가 어떤 영역으로 들어가는 것은 Functor와 Applicative Functor의 성질을 따릅니다. 모나드 역시 Functor이므로 이것이 가능합니다. 하지만 반대로 모나드는 그 내부의 값을 밖으로 꺼내는 매커니즘을 제공하지 않습니다. (물론 불가능한 것은 아닙니다.) 모나드를 순수하지 않은 부수효과를 내부에 저장해두는 컨테이너로 이해한다면 IO가 왜 모나드를 통해서 사용되어야 하는지 이해할 수 있습니다.

부수효과 자체가 순수한 함수 영역의 로직을 오염시키는 것을 차단하여 로직 자체의 순수성을 유지하면서, 거꾸로 순수한 함수를 모나드 속으로 주입하여 유용한 프로그램을 만들 수 있게 되는 것입니다. 물론 모나드는 꼭 부수효과나 비순수성과 관련된 부분에만 사용되는 도구는 아닙니다. 수학에서의 모나드 자체는 매우 추상적인 개념이라 이해하기가 어렵습니다만, 프로그래밍에서의 모나드는 적절한 수준의 개념만 알고 어떻게 쓰는지를 이해하는 것이 더욱 본질적인 요구사항이라 할 수 있습니다. 다만 모나드의 추상적인 측면이 매우 유연한 덕에 모나드는 하스켈 생태계에서 매우 폭넓게 사용되고 있습니다. 그만큼 어떻게 쓰는지를 살펴보고 공부해야할 것이 많을 뿐이죠.

댓글 남기기