[하스켈 문서 - 2] 모나드의 기초 - 2. do 표기법

redneval의 이미지

"모나드의 기초" 시리즈 이전 글 보기
[하스켈 문서 - 2] 모나드의 기초 - 1. State 모나드


3. do 표기법

(1) 기본 모나드 연산자

모나드는 형 클래스(type class)입니다. 따라서 모나드는 클래스 함수(class function)를 가지고 있습니다. 모나드의 클래스 함수는 총 4개(>>=, >>, return, fail)가 있습니다. 보통은 fail을 제외한 나머지 3개 함수를 많이 사용합니다. 그 중에서 >> 는 >>= 를 이용하여 정의할 수 있으므로 모나드 클래스 함수 중에서도 >>=와 return이 핵심입니다. 그래서 >>=와 return을 '기본 모나드 연산자(fundamental monadic operaters)'라고 합니다.

하지만 모나드를 사용하기 위해서 기본 모나드 연산자를 알 필요는 없습니다. 이전 글에서 봤듯이 기본 모나드 연산자를 사용하지 않고도 모나드를 다룰 수 있습니다. 이게 가능한 이유는 하스켈에는 모나드를 위한 전용 문법인 do 표기법이 있기 때문입니다. (통상적으로는 'do 표기법(do notation)'이라는 용어를 사용합니다. 하지만 표현식의 일종임을 강조할 목적으로 'do 표현식(do expression)'이라고도 합니다. 이 글에서는 'do 표현식'이라는 용어를 사용하도록 하겠습니다.)

기본 모나드 연산자는 당분간 잊고 do 표현식에 대해서 살펴보겠습니다.

(2) do 표현식 - 문법(Syntax)

1) 사용방법

그럼 먼저 do 표현식의 문법적인 측면을 살펴보도록 하겠습니다.

do 표현식의 사용법은 다음과 같습니다.
1) 먼저 do를 적고
2) 줄을 바꾸고(또는 공백문자를 입력하고)
3) 명령문을 1줄씩 적어가면 됩니다.

명령문(statement)에는 3가지 종류가 있습니다.
a. 모나드_표현식
b. 변수 <- 모나드_표현식
c. let 변수 = 표현식
(이 중에서 b.를 대입문(assignment statement)이라고 하며 c.를 let 명령문(let statement)이라고 합니다.)

모나드 표현식(monadic expression)은 형(type)이 모나딕 형(monadic type)인 표현식을 의미합니다.

여기서 네 가지 알아야 할 규칙이 있는데 다음과 같습니다.

규칙1. do 표현식의 모나드 표현식(대입문의 우변에 있는 모나드 표현식 포함)은 모두 같은 종류의 모나드를 사용해야합니다.
예를 들어, State 모나드와 IO 모나드를 혼용할 수 없습니다.
(둘 이상의 모나드를 같이 사용하기 위해서는 모나드 변환자(monad transformer)가 필요합니다.)
규칙2. do 표현식의 마지막 명령문은 모나드 표현식만 올 수 있으며,
해당 표현식의 결과값이 do 표현식 전체의 결과값이 됩니다.
규칙3. 대입문에서 좌변의 형(type)은 우변의 형에서 모나드를 뺀 것이 됩니다.
규칙4. let 명령문에서 좌변의 형은 우변의 형과 같습니다.

do 표현식을 사용하기 위해서 위 4개의 규칙만큼은 외워두는 것이 좋습니다.

2) moveUp 함수 분석

이전 글의 moveUp 함수를 살펴봅시다.

  1. moveUp :: Int -> State Point2D ()
  2. moveUp i = do
  3. p <- get -- 대입문(*1)
  4. put $ p {y = y p + i} -- 표현식(*2)

(*1) get의 형(type)이 다음과 같고

get :: m s

p의 형은 get의 형에서 모나드(m)를 뺀 s가 됩니다. (규칙3) 여기서 s는 Point2D이므로, p의 형은 Point2D 입니다.

(*2) put 함수의 형은 다음과 같은데,

put :: s -> m ()

여기서 s는 Point2D이고 m은 State Point2D이므로, 형 추론(type inference)된 결과는 다음과 같습니다.

put :: Point2D -> State Point2D ()

따라서 put 함수의 결과값의 형(type)은 State Point2D () 이고 마지막 명령문인 put 함수의 결과값이 do 표현식의 결과값이 되므로 (규칙2), do 표현식의 형도 State Point2D () 입니다.

(참고로, 하스켈의 형 추론(type inference)은 위에서 설명한 것과는 반대로 이뤄지지만 이해하기 쉽게 형 추론 순서와 반대로 설명했습니다.)

3) sum2' 함수 분석

이번에는, 이전 글의 sum2' 함수를 살펴봅시다.

  1. sum2' :: Num a => State (a, [a]) ()
  2. sum2' = do
  3. (s, xs) <- get -- 대입문
  4. if not $ null xs -- (*1)
  5. then do let s' = s + head xs -- let 명령문(우변의 s + head xs 가 모나드 표현식이 아니므로 let 명령문을 사용함.)
  6. let xs' = tail xs -- let 명령문(우변이 모나드 표현식이 아니므로 let 명령문을 사용함.)
  7. put (s', xs') -- 모나드 표현식
  8. sum2' -- 모나드 표현식
  9. else return ()

(*1) if-then-else 구문이 모나드 표현식이므로, do 표현식의 마지막 명령문으로 if-then-else 구문이 올 수 있습니다. (규칙2)

do 표현식에서 if-then-else 구문을 쓸 때 주의할 점은, then 뒤의 표현식과 else 뒤에 오는 표현식의 형(type)이 동일해야하며 모나딕 형이어야 한다는 점입니다. (do 표현식도 모나드 표현식의 한 종류이므로, if-then-else 구문의 then이나 else 뒤에 올 수 있습니다.)

sum2' 함수의 if-then-else 구문에서 이 점이 성립하는지 살펴봅시다. then 뒤의 do 표현식의 형은 sum2'의 형과 동일하므로 Num a => State (a, [a]) () 임을 알 수 있습니다.

또한, return 함수의 형은 다음과 같고,

return :: Monad m => a -> m a

여기서 a는 ()이고 m은 Num a => State (a, [a])이므로, return () 의 형은 Num a => State (a, [a]) () 입니다. 그러므로 then 뒤의 표현식(do 표현식)과 else 뒤의 표현식(return ())의 형이 같고, 모나딕 형임을 확인할 수 있습니다.

한 가지 재미있는 점은, do 표현식도 모나드 표현식의 한 종류이므로 do 표현식의 마지막 명령문으로 do 표현식이 올 수 있다는 점입니다. (규칙2)
이 점을 이용하면 다음과 같이 do를 중첩해서 사용하는 것도 가능합니다.

  1. main :: IO ()
  2. main = print $ execState (do
  3. moveUp 1
  4. do moveRight 2
  5. do moveUp 3
  6. do moveRight 4
  7. ) Point2D {x = 0, y = 0}

하지만 불필요하게 do를 중첩해서 사용할 필요는 없으며, 일반적으로는 다음과 같이 작성할 것입니다.

  1. main :: IO ()
  2. main = print $ execState (do
  3. moveUp 1
  4. moveRight 2
  5. moveUp 3
  6. moveRight 4
  7. ) Point2D {x = 0, y = 0}

(3) do 표현식 - 의미(Semantics)

do 표현식의 문법만 안다고 모나드를 쓸 수 있는 것은 아닙니다. 3가지 종류의 명령문(모나드 표현식, 대입문, let 명령문)이 의미하는 바를 알아야합니다.
let 명령문의 경우는 간단합니다. "let ... in ..." 구문과 동일한 역할을 합니다. 하지만 나머지 2가지 종류의 명령문은 모나드 표현식을 포함하고 있는데, 모나드 표현식이란 그 결과값의 형(type)이 모나딕 형(monadic type)인 표현식입니다. 바꿔말하면, 결과값이 모나딕 값(monadic value)인 표현식입니다.
모나딕 값(monadic value)이 비모나딕 값(non-monadic value)과 어떻게 다른지 살펴봐야하는데, 모나딕 값이 갖는 의미가 모나드의 종류마다 다르며 모나딕 값의 의미를 파악하는 것이 각 모나드를 이해하는데 있어 핵심입니다.
여기서는 State 모나드에 대해서만 살펴보도록 하겠습니다.

1) 모나딕 형(monadic type)과 모나딕 값(monadic value)

다시 한번 moveUp 함수를 살펴봅시다.

  1. moveUp :: Int -> State Point2D ()
  2. moveUp i = do
  3. p <- get :: (State Point2D) Point2D
  4. (put :: Point2D -> (State Point2D) ()) $ p {y = y p + i}

(이해를 돕기 위해서 get과 put 함수에 형 정보를 추가했습니다.)

숨어있는 상태를 불러오기 위해서 get 함수를 사용했습니다. get 함수의 형은 다음과 같은데,

get :: m s

moveUp 함수내에서는 다음과 같이 쓰였습니다.

get :: (State Point2D) Point2D

m은 모나드를 나타내는 형 변수로서, 여기서 m은 (State Point2D)으로 사용됐습니다. (참고로 s는 Point2D로 사용됐습니다.)
m(여기서는 State Point2D)은 모나딕(monadic)이라고 읽으면 됩니다. 따라서 get의 형(type)은 '모나딕 s'(여기서는 '모나딕 Point2D')라고 할 수 있습니다.

상태를 바꿔쓰기 위해서는 put 함수를 사용했습니다. put 함수의 형은 다음과 같은데,

put :: s -> m ()

moveUp 함수내에서는 다음과 같이 쓰였습니다.

put :: Point2D -> (State Point2D) ()

따라서 put은 s(여기서는 Point2D)를 인자로 받아서 '모나딕 ()'를 반환하는 함수라고 할 수 있습니다.

'모나딕 Point2D'와 '모나딕 ()'처럼 모나딕이 붙은 형(type)을 모나딕 형(monadic type)이라고 하며, 모나딕 형을 갖는 값들을 모니딕 값(monadic value)이라고 합니다.

비모니딕 값 비모나딕 형 모나딕 값 모나딕 형
3 Int 모나딕 3 모나딕 Int
Point2D {x = 2, y = 3} Point2D 모나딕 Point2D {x = 2, y = 3} 모니딕 Point2D
() () 모나딕 () 모나딕 ()

(여기서 '모나딕'은 모나드의 종류마다 달라집니다. 예를 들어, moveUp 함수에서 '모나딕 Point2D'는 'State Point2D Point2D'입니다.)

앞서 언급했듯이, 모나딕 값이 갖는 의미는 모나드 종류마다 다릅니다. State 모나드에서 모나딕 값이 비모나딕 값과 다른 점은, ('상태'라고 불리는) 숨겨진 값를 포함하고 있다는 점입니다. 예를 들어, '모나딕 3'은 '3'과 다르게 숨겨진 '상태'를 갖고 있습니다.

2) 모나딕 값을 다루는 방법

모나딕 값과 비모나딕 값을 다루는 기본적인 방식은 다음 3가지(a. ~ c.)가 있습니다.

a. 비모나딕 값을 모나딕 값으로 변환할 때는 return 함수를 사용합니다.

return :: Monad m => a -> m a

return 함수는 비모나딕 값을 인자로 받아서 모나딕 값을 반환합니다. State 모나드의 경우, return 함수는 상태는 읽거나 변화시키지 않고 단지 비모나딕 값을 모나딕 값으로 바꾸는 역할을 합니다.
return 함수를 어떤 용도로 사용하냐면, do 표현식 전체의 결과값은 do 표현식의 마지막 모나드 표현식의 결과값에 의해 결정되는데 일반적으로 return 함수는 do 표현식의 마지막에 와서 do 표현식의 결과값을 정해주는 역할을 합니다.

b. 모나딕 값을 비모나딕 값으로 변환할 때는 대입문을 사용합니다.

변수 <- 모나딕_값

위와 같이 대입문을 사용하면, 모나딕 값에서 비모나딕 값을 뽑아서 변수에 넣습니다.

State 모나드에서는 상태를 읽어올 때 get 함수를 사용합니다.

get :: m s

get 함수의 형에서 알 수 있듯이 상태를 모나딕 값을 반환하므로 비모나딕 값으로 바꾸려면 다음과 같이 대입문을 사용해야합니다.

a <- get -- 상태의 값을 읽어서(get 함수), 비모나딕 값으로 바꾸고 a라는 변수에 저장합니다.(대입문)

c. 비모나딕 값을 다룰 때는 let 명령문을 사용합니다.

참고로 모나딕 값을 다루는 방법은 위의 a. ~ c. 중에 없는데, 모나딕 값을 다룰 때는 모나딕 값을 비모나딕 값으로 바꾸고(b.) 비모나딕 값은 let 명령문으로 바꾸고(c.) 비모나딕 값을 모나딕 값으로 바꾸면(a.) 됩니다.

위의 3가지 방식은 모든 종류의 모나드에 공통적으로 사용할 수 있으며, 각 모나드에서만 사용할 수 있는 함수들도 있습니다.

State 모나드에는 다음의 함수들이 있습니다.

put    :: s -> m ()        -- 인자를 받아서 상태로 저장합니다.
modify :: (s -> s) -> m ()

사용법은 다음과 같습니다.

put "Hello"       -- 상태의 값에 "Hello"를 저장합니다.
modify (+1)       -- 상태의 값에 1을 더합니다.

(4) 결론

do 표현식의 문법(Syntax)와 의미(Semantics)를 살펴봤습니다. 컴파일 오류 없이 무사히 컴파일하기 위해서 do 표현식의 문법(특히, 앞에서 언급한 4개의 규칙)을 숙지해야합니다. 어떤 모나드를 사용하더라도 문법은 동일한 반면에, 모나드의 종류마다 모나딕 값의 의미는 다릅니다. 이번 글에서는 State 모나드를 살펴봤으며, 나머지 모나드에 대해서는 이후의 글들에서 살펴보겠습니다.

(5) 참고 문서

do 표현식 : https://www.haskell.org/onlinereport/haskell2010/haskellch3.html#x8-470003.14
모나드 : https://www.haskell.org/onlinereport/haskell2010/haskellch13.html#x21-19400013.1

Forums: 

댓글 달기

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
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.