최근 수정 시각 : 2023-08-26 05:18:06

Haskell/모나드


파일:상위 문서 아이콘.svg   상위 문서: Haskell
프로그래밍 언어 문법
{{{#!wiki style="margin: -16px -11px; word-break: keep-all" <colbgcolor=#0095c7><colcolor=#fff,#000> 언어 문법 C( 포인터) · C++( 자료형 · 특성 · 클래스 · 이름공간 · 상수 표현식) · C# · Java · Python · Kotlin · MATLAB · SQL · PHP · JavaScript
마크업 문법 HTML · CSS
개념과 용어 함수 · 인라인 함수 · 고차 함수 · 람다식 · 리터럴 · size_t · 상속 · 예외 · 조건문 · 참조에 의한 호출 · eval
기타 == · === · NaN · null · undefined · 모나드 · 배커스-나우르 표기법
프로그래밍 언어 예제 · 목록 · 분류 }}}

1. 개요2. 총론: 모나드의 정의
2.1. 함자2.2. 적용자2.3. 모나드
3. 각론: 각 모나드의 기능
3.1. Identity3.2. Maybe3.3. Either3.4. \[\]3.5. NonEmpty3.6. Proxy3.7. (,)3.8. (->)3.9. State3.10. ST3.11. IO

1. 개요

프로그래밍 언어 하스켈의 주요 개념인 모나드(Monad)에 관한 정확한 이해를 돕기 위한 문서이다.

2. 총론: 모나드의 정의

모나드란 타입 생성자(type constructor)[1] 중 후술할 성질들을 만족하는 것을 의미한다.

모나드의 정확한 의미를 파악하기 위해서는 우선 함자(Functor)와 적용자(Applicative)[2]에 관한 이해가 우선 필요하다. 모나드의 상위개념으로 적용자가, 적용자의 상위개념으로 함자가 있기 때문이다.

2.1. 함자

타입 생성자 f에 대해 적절한 함수 fmap :: (a -> b) -> f a -> f b이 존재하여 다음 성질들을 만족할 때, f함자(Functor)라고 한다.
  • 항등함수 id :: a -> a에 대해 fmap id :: f a -> f a도 항등함수다.
  • 모든 함수 f :: b -> cg :: a -> b에 대해 fmap (f . g) ≡ fmap f . fmap g[3]이다.

2.2. 적용자

함자 f에 대해 적절한 함수 pure :: a -> f a(<*>) :: f (a -> b) -> f a -> f b[4]가 존재하여 다음 성질들을 만족할 때, f적용자(Applicative)라고 하며, 임의의 타입 a에 대해 f a액션(action)이라 한다[5].
  • 모든 함수 f :: a -> b와 액션 v :: f a에 대해 pure f <*> v ≡ fmap f v이다.[6]
  • 모든 액션 u :: f (a -> b)와 값 y :: a에 대해 u <*> pure y ≡ fmap (\f -> f y) u이다.
  • 모든 액션 u :: f (b -> c), v :: f (a -> b)w :: f a에 대해 fmap (.) u <*> v <*> w ≡ u <*> (v <*> w)이다.

2.3. 모나드

적용자 m에 대해 적절한 함수 (>>=) :: m a -> (a -> m b) -> m b[7]가 존재하여 다음 성질들을 만족할 때, m모나드(Monad)라고 한다.
  • 모든 값 x :: a와 함수 k :: a -> m b에 대해 pure x >>= k ≡ k x이다.
  • 모든 액션 u :: m a에 대해 u >>= pure ≡ u이다.
  • 모든 액션 u :: m a, 함수 k :: a -> m bh :: b -> m c에 대해 u >>= (\x -> k x >>= h) ≡ u >>= k >>= h이다.
  • 모든 액션 u :: m a와 함수 f :: a -> b에 대해 u >>= (pure . f) ≡ fmap f u이다. [8]
  • 모든 액션 u :: m (a -> b)v :: m a에 대해 u >>= (\f -> v >>= (\x -> pure (f x))) ≡ u <*> v이다. [9]

하스켈에서는 모나드를 편리하게 다루게 해 주는 do 문법을 제공한다. do를 쓰고 다음 줄부터 들여쓰기를 하거나 중괄호와 세미콜론을 써서 사용할 수 있는데, 예를 들자면:
do {f <- u; x <- v; pure (f x)}

이것은 u >>= (\f -> v >>= (\x -> pure (f x)))와 같은 뜻이다. 즉 각 줄(마지막 줄은 빼고)마다 <- 앞에 있는 것을 함수의 인자(parameter)로 취급하여 그 다음 줄로 보내며, 각 줄은 >>=로 연결된다. 한편, <-가 없다면 거기에 해당하는 함수는 그 인자를 무시한다[10]는 것을 의미한다.

3. 각론: 각 모나드의 기능

여기까지 읽어봤더라도 모나드가 대체 무엇인지 아리송할 것이다. 모나드 자체로는 너무나도 추상적이고 함축적인 의미를 가지고 있기 때문이기도 하고, 아직까지는 모나드의 정의만 나왔을 뿐, 모나드를 구성하는 함수인 pure>>=에 관한 정의는 안 나왔기 때문이기도 하다. 따라서 각론에서는 각 모나드에 대해 pure>>=의 정의를 살펴보겠다. 이 정의들로부터 위 정의가 충족됨을 확인하는 것도 모나드를 이해하기 위한 좋은 연습이다.

간결한 설명을 위해, 각 모나드의 구조만을 추리고, 실제로 구현된 코드를 복붙하지 않는다. 특히 pure>>=는 원래 Applicative, Monad 클래스의 메서드지만, 여기선 그냥 함수처럼 서술한다.

여기선 base 패키지에서 제공하는 모나드만 서술돼 있지만, 다른 패키지에서는 다른 모나드도 제공하며, 필요하다면 모나드를 직접 만들어 쓸 수도 있다.

3.1. Identity

newtype Identity a = Identity a
pure = Identity
Identity x >>= k = k x

값을 생성자 Identity를 통해 그대로 담는, 아무런 기능이 없는 모나드이다.

3.2. Maybe

data Maybe a = Nothing | Just a
pure = Just
Nothing >>= _ = Nothing
Just x >>= k = k x

값을 Just를 통해 담을 수도 있지만, Nothing을 통해 값이 없을 수도 있다는 것을 의미한다. 값이 없다면 >>=를 통하더라도 결과값은 여전히 "값 없음"이며, fmap<*>도 마찬가지다. 흔히 오류를 나타내기 위해 사용한다.

3.3. Either

data Either e a = Left e | Right a
pure = Right
Left err >>= _ = Left err
Right x >>= k = k x
Maybe가 단순히 오류만을 나타낸다면, Either e무슨 오류인지도 타입 e로서 나타낼 수 있다. Left가 오류임을, Right가 정상임을 의미하는데, 영단어 right에 "오른쪽"이란 뜻도 있지만, "옳다", "바르다"라는 뜻도 있다는 것으로 외울 수 있다.

3.4. \[\]

data [a] = [] | a : [a]
pure x = [x]
[] >>= _ = []
x : xs >>= k = k x ++ (xs >>= k)
[]리스트, 즉 값의 늘어놓음을 의미한다. 생성자 []를 통해 값을 안 (즉 0개) 담을 수도 있으며, :를 통해 값을 앞에 추가할 수도 있다. 여기서 pure는 주어진 값을 1개 담으며, >>=는 각 값에 함수를 매핑하여 리스트들을 만든 다음 이어붙인다. (즉 Data.List 모듈의 concatMap과 같다.) 값이 없으면 오류를 나타낸다는 점에서는 Maybe의 역할도 모두 수행할 수 있으나, 값을 여러 개 담음으로써 중의성을 나타낼 수도 있다.

3.5. NonEmpty

data NonEmpty a = a :| [a]
(pure>>=는 생략)
Data.List.NonEmpty 모듈에서 제공하며, 값이 적어도 하나 들어 있음이 보장된 리스트라고 할 수 있다. 즉 오류는 나타낼 수 없지만, 중의성은 나타낼 수 있다.

3.6. Proxy

data Proxy a = Proxy
pure _ = Proxy
_ >>= _ = Proxy
Data.Proxy 모듈에서 제공한다. 인자로 주어진 타입을 무시하는 황당한 모나드. 모종의 이유로 값 없이 타입만 제공해야 할 때 사용한다.

3.7. (,)

data (a, b) = (a, b)
pure x = (mempty, x)
(s, x) >>= k = let (t, y) = k x in (s <> t, y)
출력자(Writer) 모나드라고 불린다. 각 액션에서 콤마의 왼쪽에 있는 값들을 모노이드(Monoid)에 출력한다. pure을 사용할 때 모노이드의 항등원이 공백의 역할을 수행한다.

3.8. (->)

(a -> ba가 정의역이고 b가 공역인 함수들의 모임을 나타낸다.)
pure x = \_ -> x
f >>= k = \r -> k (f r) r
입력자(Reader) 모나드라고 불린다. 각 액션은 인자가 입력되길 대기하는 행위로 취급되어, pure는 인자를 무시하고, >>=는 인자를 각 액션에 대해 입력한다.

3.9. State

newtype State s a = State (s -> (a, s))
pure x = State (\s -> (x, s))
State m >>= k = State (\s -> let {(x, t) = m s; State n = k x} in n t)
이 모나드는 base 패키지에서 제공하진 않으나, 후술할 모나드들을 이해하는 데 핵심이 된다. State s는 타입 s로써 상태를 가지는 기능을 수행한다. 이 "상태"는 마음대로 읽히거나 덧쓰일 수 있다. 즉 이 모나드는 함수형 언어가 명령형 언어의 기능도 수행할 수 있음을 증명한다! 물론 상태의 초기값은 필요하다.

3.10. ST

Control.Monad.ST 또는 Control.Monad.ST.Lazy 모듈에서 제공한다. 전자는 각 액션이 strict하게, 후자는 각 액션이 lazy하게 실행된다[11]. State s 모나드는 그 상태의 타입이 s여야 한다는 제약이 있는데, 그 제약을 완화한 것이 ST이다. ST에 들어 있는 상태를 다루기 위해서는 Data.STRef 또는 Data.STRef.Lazy 모듈에 들어 있는 다음 함수들이 필요하다[12]:
  • newSTRef :: a -> ST s (STRef s a) : 타입이 a인 변수를 생성한 다음, 입력값으로 초기화한다.
  • readSTRef :: STRef s a -> ST s a : 변수에서 값을 읽어들인다.
  • writeSTRef :: STRef s a -> a -> ST s () : 변수에 값을 덧씌운다.
  • modifySTRef :: STRef s a -> (a -> a) -> ST s () : 변수의 값을 함수를 통해 변경한다.

보다시피 모두 액션을 내놓는 함수인데, 이렇게 얻은 ST s a에서 타입 a를 가진 결과값을 얻으려면 runST 함수를 사용한다.

3.11. IO

ST 모나드의 상태의 타입에는 제약이 없다고 하였다. 그렇다면 컴퓨터 그 자체[13]를 상태의 타입으로 삼을 수 있지 않을까? 그리하여 대망의 IO 모나드를 얻게 된다. 그 이름에서 알 수 있듯이, 이 모나드는 현실에서의 입출력을 담당한다. 이게 가능한 이유는 이 모나드가 현실의 입력장치 및 출력장치인 마우스, 키보드, 모니터, 스피커 등등 그 자체를 변수로 취급하기 때문이다[14]. 당장 CUI 환경에서의 기초적인 입출력을 담당하는 putStrLn :: String -> IO ()getLine :: IO String부터가 액션 혹은 액션을 내놓는 함수이다.

물론 IO에서도 변수를 만들 수 있는데, Data.IORef 모듈에 들어 있는 다음 함수들을 쓰면 된다:
  • newIORef :: a -> IO (IORef a)
  • readIORef :: IORef a -> IO a
  • writeIORef :: IORef a -> a -> IO ()
  • modifyIORef :: IORef a -> (a -> a) -> IO ()

[1] Maybe, [] 등 타입을 인자로 넣으면 타입이 나오는 것들을 의미한다. [2] 통일된 번역이 아직 없어 임의로 번역하였다. [3] 는 함수로서 같다는 걸 의미한다. 컴퓨터가 일반적인 경우에서 이를 증명할 수 없으므로 ==가 아니라 로 표기한다. [4] apply라고 읽으며, 결합방향(fixity)은 왼쪽이다. 즉 u <*> v <*> w(u <*> v) <*> w이다. [5] 다만, f가 모나드라야 액션이라고 부르는 경우가 더 많다. [6] 이 성질을 이용해 만든 fmapliftA라고 하여, Control.Applicative 모듈에서 제공한다. [7] bind라고 읽으며, 결합방향은 왼쪽이다. [8] 이 성질을 이용해 만든 fmapliftM이라고 하여, Control.Monad 모듈에서 제공한다. [9] 이 성질을 이용해 만든 (<*>)ap이라고 하여, 역시 Control.Monad 모듈에서 제공한다. [10] \_로 시작하는 함수. [11] 액션으로 구분되는 각 단계가 그러할 뿐, 그 입출력값은 여전히 기본적으로 lazy하므로 주의해야 한다. [12] State의 조건을 완화했다면서 여전히 s를 인자로 받는 모습을 볼 수 있는데, 어차피 이 함수들 모두 s를 제한하지 않으므로, 프로그래머 입장에서는 무시해도 된다. [13] 아예 현실 그 자체라고 해석해도 된다. [14] 반대로 말하면 이게 가능한 모나드는 IO밖에 없으므로, 현실의 모든 입출력은 IO를 통할 수밖에 없다.