Haskell/모나드
(♥ 0)
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 -> c
와g :: 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 b
와h :: 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. (,)[편집]
출력자(Writer) 모나드라고 불린다. 각 액션에서 콤마의 왼쪽에 있는 값들을 모노이드(Monoid)에 출력한다.
data (a, b) = (a, b)
pure x = (mempty, x)
(s, x) >>= k = let (t, y) = k x in (s <> t, y)
pure
을 사용할 때 모노이드의 항등원이 공백의 역할을 수행한다.3.8. (->)[편집]
입력자(Reader) 모나드라고 불린다. 각 액션은 인자가 입력되길 대기하는 행위로 취급되어,(
a -> b
는a
가 정의역이고b
가 공역인 함수들의 모임을 나타낸다.)
pure x = \_ -> x
f >>= k = \r -> k (f r) r
pure
는 인자를 무시하고, >>=
는 인자를 각 액션에 대해 입력한다.3.9. State[편집]
이 모나드는 base 패키지에서 제공하진 않으나, 후술할 모나드들을 이해하는 데 핵심이 된다.
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)
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 ()
Maybe
, []
등 타입을 인자로 넣으면 타입이 나오는 것들을 의미한다.[2] 통일된 번역이 아직 없어 임의로 번역하였다.[3] ≡
는 함수로서 같다는 걸 의미한다. 컴퓨터가 일반적인 경우에서 이를 증명할 수 없으므로 ==
가 아니라 ≡
로 표기한다.[4] apply라고 읽으며, 결합방향(fixity)은 왼쪽이다. 즉 u <*> v <*> w
는 (u <*> v) <*> w
이다.[5] 다만, f
가 모나드라야 액션이라고 부르는 경우가 더 많다.[6] 이 성질을 이용해 만든 fmap
은 liftA
라고 하여, Control.Applicative
모듈에서 제공한다.[7] bind라고 읽으며, 결합방향은 왼쪽이다.[8] 이 성질을 이용해 만든 fmap
은 liftM
이라고 하여, Control.Monad
모듈에서 제공한다.[9] 이 성질을 이용해 만든 (<*>)
는 ap
이라고 하여, 역시 Control.Monad
모듈에서 제공한다.[10] 즉 \_
로 시작하는 함수.[11] 액션으로 구분되는 각 단계가 그러할 뿐, 그 입출력값은 여전히 기본적으로 lazy하므로 주의해야 한다.[12] State
의 조건을 완화했다면서 여전히 s
를 인자로 받는 모습을 볼 수 있는데, 어차피 이 함수들 모두 s
를 제한하지 않으므로, 프로그래머 입장에서는 무시해도 된다.[13] 아예 현실 그 자체라고 해석해도 된다.[14] 반대로 말하면 이게 가능한 모나드는 IO
밖에 없으므로, 현실의 모든 입출력은 IO
를 통할 수밖에 없다.