Haskell/모나드

덤프버전 :


파일:나무위키+상위문서.png   상위 문서: Haskell



1. 개요
2. 총론: 모나드의 정의
2.1. 함자
2.2. 적용자
2.3. 모나드
3. 각론: 각 모나드의 기능
3.1. Identity
3.2. Maybe
3.3. Either
3.4. \[\]
3.5. NonEmpty
3.6. Proxy
3.7. (,)
3.8. (->)
3.9. State
3.10. ST
3.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 -> 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. (,)[편집]


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하게 실행된다[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 ()
파일:크리에이티브 커먼즈 라이선스__CC.png 이 문서의 내용 중 전체 또는 일부는 2023-10-27 07:26:21에 나무위키 Haskell/모나드 문서에서 가져왔습니다.

[1]
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
를 통할 수밖에 없다.