최근 수정 시각 : 2024-09-16 21:45:17

Haskell/특징/모나드


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

1. 개요2. 모나드의 정의
2.1. 함자(Functor)2.2. 적용성 함자(Applicative Functor)2.3. 모나드(Monad)
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. 모나드의 정의

모나드란 * -> *의 종(kind)을 가지는 타입 연산자(type operator) 중 후술할 성질을 만족하는 것을 의미한다.(타입 연산자란 Maybe[] 등 타입을 인자로 넣으면 타입이 나오는 것을 의미한다.)
Maybe :: * -> *
[]    :: * -> *
모나드의 정확한 의미를 파악하기 위해서는 우선 함자(functor)와 적용성 함자(applicative functor)에 관한 이해가 필요하다.(통일된 번역이 아직 없어 임의로 번역하였다.) 모나드의 상위 개념으로 적용성 함자가, 적용성 함자의 상위 개념으로 함자가 있기 때문이다.

파일:functors.svg

2.1. 함자(Functor)

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

2.2. 적용성 함자(Applicative Functor)

class Functor f => Applicative f where
  pure  :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b
함자 f에 대해 적절한 함수 pure<*>[2]가 존재하여 다음 성질들을 만족할 때, f적용성 함자라고 하며, 임의의 타입 a에 대해 f a액션(action)이라 한다.[3]
  • 모든 함수 f :: a -> b와 액션 v :: f a에 대해 pure f <*> v ≡ fmap f v이다.[4]
  • 모든 액션 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. 모나드(Monad)

class Applicative m => Monad m where
  (>>=)  :: m a -> m (a -> b) -> m b
  return :: a -> m a
적용성 함자 m에 대해 적절한 함수 >>=가 존재하여 다음 성질들을 만족할 때, m모나드(Monad)라고 한다.(>>=는 bind라고 읽으며, 결합방향은 왼쪽이다.)
  • 모든 값 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이다. [5]
  • 모든 액션 u :: m (a -> b)v :: m a에 대해 u >>= (\f -> v >>= (\x -> pure (f x))) ≡ u <*> v이다. [6]

하스켈에서는 모나드를 편리하게 다루게 해 주는 do 문법을 제공한다. do를 쓰고 다음 줄부터 들여쓰기를 하거나 중괄호와 세미콜론을 써서 사용할 수 있는데, 예를 들자면:
do {
  f <- u;
  x <- v;
  pure (f x)
} 
이것은 아래 식과 같은 뜻이다.
u >>= (\f -> v >>= (\x -> pure (f x)))
즉, 각 줄(마지막 줄은 빼고)마다 <- 앞에 있는 것을 함수의 인자(parameter)로 취급하여 그 다음 줄로 보내며, 각 줄은 >>=로 연결된다.

한편, <-가 없다면 거기에 해당하는 함수는 그 인자를 무시한다는 것을 의미한다.(즉 \_로 시작하는 함수.)

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 -> b는 a가 정의역이고 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하게 실행된다[7]. State s 모나드는 그 상태의 타입이 s여야 한다는 제약이 있는데, 그 제약을 완화한 것이 ST이다. ST에 들어 있는 상태를 다루기 위해서는 Data.STRef 또는 Data.STRef.Lazy 모듈에 들어 있는 다음 함수들이 필요하다[8]:
  • 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 모나드의 상태의 타입에는 제약이 없다고 하였다. 그렇다면 컴퓨터 그 자체[9]를 상태의 타입으로 삼을 수 있지 않을까? 그리하여 대망의 IO 모나드를 얻게 된다. 그 이름에서 알 수 있듯이, 이 모나드는 현실에서의 입출력을 담당한다. 이게 가능한 이유는 이 모나드가 현실의 입력장치 및 출력장치인 마우스, 키보드, 모니터, 스피커 등등 그 자체를 변수로 취급하기 때문이다[10]. 당장 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] 는 함수로서 같다는 걸 의미한다. 컴퓨터가 일반적인 경우에서 이를 증명할 수 없으므로 ==가 아니라 로 표기한다. [2] apply라고 읽으며, 결합방향(fixity)은 왼쪽이다. 즉 u <*> v <*> w(u <*> v) <*> w이다. [3] 다만, f가 모나드라야 액션이라고 부르는 경우가 더 많다. [4] 이 성질을 이용해 만든 fmapliftA라고 하여, Control.Applicative 모듈에서 제공한다. [5] 이 성질을 이용해 만든 fmapliftM이라고 하여, Control.Monad 모듈에서 제공한다. [6] 이 성질을 이용해 만든 (<*>)ap이라고 하여, 역시 Control.Monad 모듈에서 제공한다. [7] 액션으로 구분되는 각 단계가 그러할 뿐, 그 입출력값은 여전히 기본적으로 lazy하므로 주의해야 한다. [8] State의 조건을 완화했다면서 여전히 s를 인자로 받는 모습을 볼 수 있는데, 어차피 이 함수들 모두 s를 제한하지 않으므로, 프로그래머 입장에서는 무시해도 된다. [9] 아예 현실 그 자체라고 해석해도 된다. [10] 반대로 말하면 이게 가능한 모나드는 IO밖에 없으므로, 현실의 모든 입출력은 IO를 통할 수밖에 없다.