[하스켈 문서 - 2] 모나드의 기초 - 1. State 모나드

redneval의 이미지

1. 서문

(1) 모나드 튜토리얼

하스켈에서 모나드는 중요한 부분을 차지하고 있습니다. 하지만 추상적인 개념이다보니 많은 이들이 이해하는데 어려움을 겪었고, 모나드를 설명하기 위한 많은 모나드 튜토리얼들이 작성됐습니다. 저는 그동안 모나드 튜토리얼을 작성하고 싶지는 않은 몇 가지 이유가 있었는데, 그 중 하나는 모나드 튜토리얼이 (비록 대부분이 영어 문서이기는 하지만) 이미 많이 있기 때문이고, 또 한 가지 이유는 기존의 모나드 튜토리얼 보다 더 훌륭한 글을 쓸 자신이 없었기 때문이었습니다. 그런데 최근에 모나드 괴담(https://e.xtendo.org/haskell/ko/monad_fear/slide)을 읽고 약간의 영감을 얻고는 모나드에 관한 글을 작성하려고 합니다. (다만, 제가 모나드 괴담이라는 글의 내용에 모두 동의하는 것은 아님을 밝혀둡니다.)

기존의 모나드 튜토리얼과의 주요 차이점은 다음과 같습니다.

* 모나드가 무엇인지 설명하기 보다는 모나드로 무엇을 할 수 있는지와 어떻게 사용하는지에 초점을 맞춥니다.
* 모나드를 먼저 설명하기 보다는 State 모나드, ST 모나드, IO 모나드 등을 먼저 설명합니다.
* 추상적인 설명 또는 수학적인 설명을 하기 보다는 코드를 보며 설명합니다.


(2) 사전 준비

본격적으로 시작하기 전에 하스켈 개발환경을 갖춰야 합니다. 아직 하스켈 개발환경을 갖추지 않은 분들은 다음 글을 먼저 읽고 Haskell Platform과 Atom을 설치하기 바랍니다.

* [하스켈 문서 - 1] Atom과 Cabal을 이용하는 개발 환경 설정(https://kldp.org/node/155123)

또한 본 문서를 읽는 독자는 하스켈의 기초적인 내용을 알고 있다고 가정합니다. 하스켈의 기초적인 내용을 배우기 위한 곳으로 https://wikidocs.net/book/204 를 추천합니다. 그 곳의 "하스켈 기초", "하스켈 초급", "하스켈 중급"을 읽으면 충분할 것입니다.


2. State 모나드

(1) 참조적 투명성

State 모나드를 이해하기 위해서 먼저 참조적 투명성부수 효과라는 개념을 알 필요가 있습니다. 참조적으로 투명한 함수는 인자가 같으면 같은 값을 반환합니다. 다음의 C 코드를 생각해봅시다.

  1. int moveUp(int inc)
  2. {
  3. static int x = 0;
  4. x += inc;
  5. return x;
  6. }
  7.  
  8. int main()
  9. {
  10. int a, b, c;
  11. a = moveUp(1);
  12. b = moveUp(1);
  13. c = moveUp(1);
  14. printf("a = %d, b = %d, c = %d\n", a, b, c);
  15. }

실행 결과는 다음과 같습니다.

a = 1, b = 2, c = 3

moveUp 함수에 같은 인자(여기서는 1)를 주었으나 반환값은 다르게 나옵니다. 그러므로 moveUp 함수는 참조적으로 투명하지 않습니다. 이는 moveUp 함수 내부에서 static 변수인 x(1차원 좌표를 의미합니다.)의 값을 바꾸기 때문인데, 이런 식으로 반환값 이외에도 "상태"(여기서는 변수 x의 값)를 바꾸는 것을 부수 효과(side effect)라고 합니다. 하스켈은 순수 함수형 언어이므로 "모든" 함수는 참조적으로 투명해야 합니다. 그래서 하스켈에서 부수 효과를 갖는 함수를 구현하려면 함수의 형(type)에 부수 효과를 명시적으로 나타내야 합니다.


(2) 부수 효과 - 인자를 통해 전달하는 방식

위에서는 1차원 좌표였는데 여기서는 2차원 좌표로 생각해봅시다.

  1. module Main where
  2.  
  3. import Data.Function ( (&) )
  4.  
  5. data Point2D = Point2D {x :: Int, y :: Int}
  6. deriving (Show)
  7.  
  8. moveUp :: Int -> Point2D -> Point2D
  9. moveUp i p = p {y = y p + i}
  10.  
  11. moveDown :: Int -> Point2D -> Point2D
  12. moveDown i p = p {y = y p - i}
  13.  
  14. moveRight :: Int -> Point2D -> Point2D
  15. moveRight i p = p {x = x p + i}
  16.  
  17. moveLeft :: Int -> Point2D -> Point2D
  18. moveLeft i p = p {x = x p - i}
  19.  
  20. main :: IO ()
  21. main = print $ Point2D {x = 0, y = 0}
  22. & moveUp 3
  23. & moveRight 2

& 연산자는 $ 연산자와 처럼 코드를 읽기 편하게 만들기 위해 사용한 것으로 x & f = f x 로 정의됩니다. 2차원 좌표를 나타내기 위해 Point2D 라는 자료형을 정의해서 사용했습니다. moveUp, moveDown, moveRight, moveLeft 함수들은 말 그대로 좌표를 이동시키는 함수입니다. main 함수 부분을 보면, 처음에는 (0, 0)으로 시작해서 위로 3칸 옮기면 (0, 3)이 되고 오른쪽으로 2칸 옮기면 (2, 3)이 됩니다. 따라서 위 코드를 실행하면 다음과 같이 출력됩니다.

Point2D {x = 2, y = 3}

2차원 좌표라는 상태를 변화시키는 부수 효과를 나타내기 위해서 함수의 형(type)의 인자(parameter)와 결과(return)에 Point2D를 명시해주었습니다.


(3) 부수 효과 - State 모나드를 이용한 방식

State 모나드를 이용해서 move 함수들을 구현해봤습니다. 다만 moveDown과 moveLeft 함수는 생략했습니다.

  1. module Main where
  2.  
  3. import Control.Monad.State
  4.  
  5. data Point2D = Point2D {x :: Int, y :: Int}
  6. deriving (Show)
  7.  
  8. moveUp :: Int -> State Point2D ()
  9. moveUp i = do
  10. p <- get
  11. put $ p {y = y p + i}
  12.  
  13. moveRight :: Int -> State Point2D ()
  14. moveRight i = do
  15. p <- get
  16. put $ p {x = x p + i}
  17.  
  18. main :: IO ()
  19. main = print $ execState (do
  20. moveUp 3
  21. moveRight 2
  22. ) Point2D {x = 0, y = 0}

State 모나드를 이용하면 부수 효과를 나타내기 위해서 형(type)에 State를 사용하면 됩니다. 달라진 점은 인자와 결과의 형에 Point2D라는 상태가 숨어있다는 점입니다.

숨어있는 상태를 불러오려면 get이라는 함수를 사용합니다.

get :: m s

상태를 바꿔쓰려면 put 이라는 함수를 사용합니다.

put :: s -> m ()

상태를 불러와서 바꿔쓰는 과정은, modify 함수를 이용해서 간단하게 할 수 있습니다.

modify :: MonadState s m => (s -> s) -> m ()

modify 함수를 이용해서 moveUp과 moveRight 함수를 정의하면 다음과 같습니다.

moveUp i = modify (\p -> p {y = y p + i})

moveRight i = modify (\p -> p {x = x p + i})

State 함수들을 실행하려면 다음의 함수들을 이용하면 됩니다.

runState  :: State s a -> s -> (a, s)
execState :: State s a -> s -> s
evalState :: State s a -> s -> a

이 함수들은 공통적으로 첫번째 인자로 State 함수를 받고, 두번째 인자로 초기 상태를 받습니다. 차이점은 execState는 결과값이 최종 상태(좌표), evalState의 결과값이 최종 반환값, runState는 결과값이 최종 반환값과 최종 상태의 튜플이라는 점입니다. 위의 main 함수에서 우리가 필요했던 것은 최종 상태(좌표)였으므로 execState를 사용하였습니다. 물론 runState를 사용하여 다음과 같이 쓸 수도 있습니다.

main = print $ snd $ runState (do
            moveUp 3
            moveRight 2
        ) Point2D {x = 0, y = 0}


(4) State 모나드의 기능과 사용 예제

모나드는 (나중에 다시 설명하겠지만) 어떠한 기능 하나를 부여해줍니다. 그 기능은 모나드의 종류에 따라 다른데, State 모나드의 기능은 인자를 숨겨주는 것입니다. (기본적으로 하나의 인자만 숨길 수 있지만, 하나의 인자는 튜플이 될 수도 있고 레코드 구문(record syntax)을 이용해서 하나의 함수에 여러 함수를 담을 수도 있으므로 이를 이용하여 여러 개의 인자를 숨길 수 있습니다.) 이러한 기능을 어떻게 이용할 수 있는지 살펴봅시다.

리스트을 요소의 합을 구하는 sum 함수를 만들어 봅시다. (물론 Prelude에 sum이 정의돼있으며, fold 함수를 이용해서 정의할 수도 있지만, 여기서는 재귀를 이용해서 새로 만들어봅시다.)

  1. module Main where
  2.  
  3. sum1 :: Num a => [a] -> a
  4. sum1 xs = sum1' 0 xs
  5.  
  6. sum1' :: Num a => a -> [a] -> a
  7. sum1' s xs =
  8. if not $ null xs
  9. then let s' = s + head xs
  10. xs' = tail xs
  11. in sum1' s' xs'
  12. else s
  13.  
  14. main :: IO ()
  15. main = print $ sum1 [(1 :: Integer) .. 10]

절차형 언어에서 루프를 사용하는 반면에, 함수형 언어인 하스켈에서는 재귀를 이용합니다. 재귀적으로 정의된 sum1' 함수에 초기값인 0을 적용해서 sum1 함수를 만들었습니다.

그러면 이번에는 State 모나드를 이용해서 인자를 숨겨봅시다.

  1. module Main where
  2.  
  3. import Control.Monad.State
  4.  
  5. sum2 :: Num a => [a] -> a
  6. sum2 xs = fst $ execState sum2' (0, xs)
  7.  
  8. sum2' :: Num a => State (a, [a]) ()
  9. sum2' = do
  10. (s, xs) <- get
  11. if not $ null xs
  12. then do let s' = s + head xs
  13. let xs' = tail xs
  14. put (s', xs')
  15. sum2'
  16. else return ()
  17.  
  18. main :: IO ()
  19. main = print $ sum2 [(1 :: Integer) .. 10]

sum2 함수(와 그에 대응하는 sum1 함수)는 초기값을 0으로 설정해줍니다. sum2' 함수(와 그에 대응하는 sum1' 함수)에서는 리스트에서 첫번째 요소를 뽑아서 합산해나갑니다.

달라진 점을 살펴보면, sum2' 함수의 형(type)을 살펴보면 인자(Parameter)를 받지 않는 함수라는 점을 알 수 있습니다. 인자를 State 안에 숨겼기 때문입니다. 그리고 sum2 함수는 State 함수를 다루기 위해 execState 함수를 사용했고, sum2' 함수는 State를 다루기 위해서 do 표기법과 get, put 함수를 사용했으며 재귀함수를 종료할 때 return을 사용하는 점이 다릅니다. 그 몇 가지를 제외하면 구조가 다르지 않다는 점을 알 수 있습니다.

다음에는 모나드 전용 구문인 "do 표기법"(do notation)에 대해서 살펴보겠습니다.

Forums: 
jen6의 이미지

너무 좋은글이네요. 모나드를 제일 쓸 수 있도록 하고 예제코드도 좋은데 후속편은 없나요 ㅠㅠ

redneval의 이미지

이 글에 대해서 제가 한동안 잊고 있었습니다. ㅎㅎ
후속편을 준비해보겠습니다.
다만 모나드라는 주제가 글 쓰기에 상당히 까다로운 주제라서 글 쓰는데 시간이 좀 걸릴겁니다.
느긋한 마음으로 기다리세요. ㅎㅎ

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.