계정: 로그인
AA 📝
9. 모나드 (Monads)

A Gentle Introduction to Haskell, Version 98
이전 다음 위로


Haskell을 처음 접하는 많은 사람들이 모나드 (monad) 개념 때문에 혼란을 겪습니다. Haskell에서는 모나드를 자주 만나게됩니다: 입출력 시스템이 모나드를 써서 구성되어있고, 모나드를 위한 (do 표현식 같은) 특수 구문이 제공되며, 표준 라이브러리에는 모나드에 관련된 모듈이 전부 들어있습니다. 이번 장에서는 모나드 프로그래밍에 대해 더 자세히 알아봅니다.

이 장은 아마도 다른 장들에 비해 덜 ‘gentle’할겁니다. 여기서는 모나드와 관련된 언어 특성에 대해서 언급할 뿐만 아니라, 왜 모나드가 그렇게 중요한 도구이며 어떻게 사용할 수 있는지에 관해 좀 더 큰 그림을 그려볼 것입니다. 모나드를 설명하는데에 있어서 모든 사람들에게 다 통용되는 한 가지 방법 같은건 없으며, 더 자세한 설명은 haskell.org를 찾아보시기 바랍니다. 모나드를 사용하는 실용적 프로그래밍에 대해 소개하는 또다른 좋은 문서는 Wadler의 Monads for Functional Programming [10]입니다.

9.1 모나드 관련 클래스들

Prelude에는 모나드를 정의하고 있는 클래스가 몇 가지 들어있고 Haskell에서 이들을 사용합니다. 이 클래스들은 범주론(category theory)상 모나드 구성에 기반하고 있습니다. 비록 범주론적 용어들이 모나드 클래스와 연산들의 이름을 제공하고 있지만, 모나드 클래스의 사용법을 직관적으로 이해하기 위해서 추상 수학(abstract mathematics)을 파고들 필요는 없습니다.

모나드는 IO 같은 polymorphic 타입의 기반 위에 만들어져 있습니다. 모나드 자체는 모나드 클래스 Functor, Monad, MonadPlus들 중 일부 혹은 전부와 타입을 연관시켜주는 인스턴스 선언을 통해 정의되어 있습니다. 모나드 클래스 중 어느 것도 파생 가능하지 않습니다. IO 외에도, Prelude에 있는 두 가지 또다른 타입들인 리스트([])와 Maybe모나드 클래스의 멤버입니다.

수학적으로, 모나드는 모나드 연산들과 맞아 떨어지는 법칙(laws)의 집합에 의해 좌우됩니다. 법칙에 관한 이런 발상은 모나드에만 국한된 것이 아니며, Haskell에는 최소한 비정형적으로라도 법칙에 의해 좌우되는 다른 연산들이 더 있습니다. 예를 들어, x /= ynot (x == y)비교하려는 대상 타입이 무엇이냐에 상관 없이 항상 같아야만 합니다. 그러나, 사실은 이것을 보증할 수는 없습니다. ==/=는 둘 다 클래스 Eq의 개별적인 메서드들이고, ==/=가 이런 식으로 서로 관련된다는 것을 보증할 방법이 없습니다. 마찬가지 관점에서, 여기서 제시하는 모나드 법칙들은 Haskell이 강요하는 사항은 아니지만, 모나드 클래스의 모든 인스턴스들에의해 지켜져야만 합니다. 모나드 법칙은 모나드의 기반 구조를 들여다볼 수 있게 해줍니다. 이 법칙들을 확인해 봄으로써, 모나드가 어떻게 사용되는지에 대한 느낌을 얻으실 수 있길 바랍니다.

5장에서 이미 논의한 Functor 클래스는 fmap이라는 단 하나의 연산만을 정의합니다. 함수 map은 컨테이너 속의 대상들에 어떤 연산 하나를 적용하고 (polymorphic 타입은 다른 타입의 값들을 담는 컨테이너라고 생각할 수 있습니다), 같은 모양의 컨테이너를 리턴합니다. 이런 법칙은 Functor 클래스의 fmap에도 적용됩니다:

이런 법칙들은 컨테이너의 모양이 fmap에 의해 바뀌지 않는다는 점과 컨테이너의 내용물들이 매핑 연산에 의해 재정렬되지 않는다는 점을 보증해줍니다.

Monad 클래스는 두 개의 기본 연산인 >>= (바인딩)과 return을 정의하고 있습니다.

   1 infixl 1  >>, >>=
   2 
   3 class  Monad m  where
   4     (>>=)     :: m a -> (a -> m b) -> m b
   5     (>>)      :: m a -> m b -> m b
   6     return    :: a -> m a
   7     fail      :: String -> m a
   8 
   9     m >> k    =  m >>= \_ -> k

바인드 연산 >>>>=는 두 모나드 값을 결합하는 반면 return 연산은 값을 모나드(컨테이너)에 주입합니다. >>=의 타입지정자는 이 연산을 이해하는 데에 도움이 됩니다: ma >>= \v -> mba 타입의 값을 담고 있는 모나드 값 maa 타입의 값 v에 대한 함수 하나를 결합하여 모나드 값 mb를 리턴합니다. 결과는 mamb를 결합하여 b를 담는 모나드 값으로 합치는 것입니다. 함수 >>는 첫번째 모나드 연산에 의해 발생하는 값이 필요없는 경우에 사용됩니다.

물론 바인딩의 궁극적인 의미는 모나드에 의해 좌우됩니다. 예를 들어, 입출력 모나드인 x >>= y는 두 개의 액션 중 첫번째 것의 결과를 두 번째 액션에 전달하면서 두 액션을 연속적으로 수행합니다. 다른 내장 모나드인 리스트와 Maybe 타입에 대해서는, 이런 모나드 연산들은 하나의 계산으로부터 다음번 계산으로 0개 이상의 값을 전달하는 것으로 이해할 수 있습니다. 이에 관한 예제는 곧 보시게 됩니다.

do 구문은 연쇄적인 모나드 연산들에 대한 간편한 축약형을 제공합니다. do 구문을 해석하는 핵심은 다음 두 가지 규칙을 따릅니다:

상기 두 번째 형식의 do가 refutable할 때, 패턴 매칭의 실패는 fail 연산을 호출합니다. 이것은 (입출력 모나드에서처럼) 오류를 일으키거나 혹은 (리스트 모나드에서처럼) ‘zero’를 리턴합니다. 따라서 더욱 복잡한 해석은 다음과 같으며:

여기서 "s"는 오류 메세지 속에서 사용하기 위해 do 문의 위치를 나타내는 문자열입니다. 예를 들어 입출력 모나드에서, 'a' <- getChar 같은 액션은 만일 입력된 문자가 'a'가 아니라면 fail을 호출할 것입니다. 결국, 이것은 입출력 모나드 내에서 failerror를 호출하기 때문에 프로그램을 종료시키게 됩니다.

>>=return을 지배하는 법칙은 다음과 같습니다:

클래스 MonadPluszero 원소와 plus 연산을 갖는 모나드를 위해 사용됩니다:

   1 class  (Monad m) => MonadPlus m  where
   2     mzero    :: m a
   3     mplus    :: m a -> m a -> m a

zero 원소는 다음과 같은 법칙을 따릅니다:

리스트에 대해서는, zero 값이란 빈 리스트인 []입니다. 입출력 모나드는 zero 원소를 갖지 않으며, 이 클래스의 멤버가 아닙니다.

연산자 mplus를 지배하는 법칙은 다음과 같습니다:

연산자 mplus는 리스트 모나드 내에서는 평범한 리스트 결합 연산자입니다.

9.2 내장 모나드

모나드 연산들과 그들을 지배하는 법칙이 주어진다면, 이걸로 무엇을 구성할 수 있을까요? 입출력 모나드에 대해서는 이미 자세히 살펴보았으니, 이제는 다른 두 가지 내장 모나드들에 대해서 알아보도록 합니다.

리스트에 대해서는, 모나드 바인딩은 리스트 속의 각각의 값에 대한 연산들의 집합을 하나로 묶는 것과 관련이 있습니다. 리스트와 함께 사용될 때의 >>=의 타입지정자는 다음과 같이 바뀝니다:

즉, a 타입들의 리스트와 a에서 b의 리스트로의 함수가 주어지면, 바인딩은 이 함수를 입력 중의 각각의 a들에 적용하여 그 결과로 만들어진 모든 b들을 하나의 리스트로 결합하여 리턴합니다. 함수 return은 단일 원소 리스트를 만듭니다. 이미 이런 연산들에 익숙해있어야 합니다: list comprehension은 리스트에 대해 정의된 모나드 연산들을 써서 간단히 표현될 수 있습니다. 아래 세 가지 표현식들은 모두 같은 것을 나타내는 서로 다른 구문들입니다:

   1 [(x,y) | x <- [1,2,3] , y <- [1,2,3], x /= y]

   1 do x <- [1,2,3]
   2    y <- [1,2,3]
   3    True <- return (x /= y)
   4    return (x,y)

   1 [1,2,3] >>= (\ x -> [1,2,3] >>= (\y -> return (x/=y) >>=
   2    (\r -> case r of True -> return (x,y)
   3                     _    -> fail "")))

이 정의는 이 모나드 내에서 fail이 빈 리스트로 정의되어있는 사실에 의존하고 있습니다. 중요한 점은, 각각의 <-나머지 모나드 계산에 전달되는 값들의 집합을 만들어내고 있다는 점입니다. 따라서 x <- [1,2,3]은 리스트 원소 각각에 대해서 한 번 씩, 총 세 번의 모나드 계산의 나머지를 유발시킵니다. 리턴되는 표현식 (x,y)는 이를 감싸는 바인딩의 모든 가능한 조합에 대해서 평가됩니다. 이런 면에서, 리스트 모나드는 다중 값 (multi-valued) 인수에 대한 함수를 설명하고 있다고 생각할 수 있습니다. 예를 들어 아래와 같은 함수는:

   1 mvLift2          :: (a -> b -> c) -> [a] -> [b] -> [c]
   2 mvLift2 f x y    =  do x' <- x
   3                        y' <- y
   4                        return (f x' y')

두 개의 인수를 갖는 평범한 함수(f)를 다중 값(인수의 리스트)에 대한 함수로 바꾸며, 두 입력 인수의 가능한 모든 조합들 각각에 대해서 값을 리턴합니다. 예를 들어,

이 함수는 모나드 라이브러리에 있는 함수 LiftM2의 특별판입니다. 리스트 모나드 밖에 있는 함수 하나를, 계산이 다중 값에 대해서 일어나는 리스트 모나드 내부로 전이시키는 것이라고 봐도 무방합니다.

Maybe에 대해서 정의된 모나드는 리스트 모나드와 비슷합니다: Nothing이라는 값은 []로 여겨질 수 있고, Just x[x]로 여겨질 수 있습니다.

9.3 모나드 사용하기

모나드 연산자와 그에 연관된 법칙들에 대해서 설명하는 것만으로는 사실은 모나드가 어떤 점에서 유용한지를 제대로 보여주지 못합니다. 이들이 궁극적으로 제공하는 것은 모듈성(modularity)입니다. 즉, 연산을 모나드적으로 정의함으로써, 새로운 기능이 모나드와 투명하게 공조할 수 있게끔 해 주는 방식으로 그 연산의 기반 기전을 숨길 수 있게 됩니다. Wadler의 논문 [10]은 모듈화된 프로그램을 구성하는 데에 어떻게 모나드를 활용할 수 있는지를 보여주는 훌륭한 예입니다. 먼저 이 논문에서 직접 뽑아낸 모나드인 상태 모나드(state monad)로 시작하고, 그 다음엔 비슷한 정의를 통해 좀 더 복잡한 모나드를 만들어볼 것입니다.

간단히 말해서, 상태 타입 S에 대해서 만들어진 상태 모나드는 다음과 같이 생겼습니다:

   1 data  SM a    =  SM (S -> (a,S))   -- 모나드 타입
   2 
   3 instance  Monad SM  where
   4     -- 상태 전이를 정의
   5     SM c1 >>= fc2    =  SM (\s0 -> let (r,s1) = c1 s0 
   6                                        SM c2  = fc2 r
   7                                    in c2 s1)
   8     return k         =  SM (\s -> (k,s))
   9 
  10 -- 모나드로부터 상태를 추출
  11 readSM    :: SM S
  12 readSM    =  SM (\s -> (s,s))
  13 
  14 -- 모나드의 상태를 갱신
  15 updateSM      :: (S -> S) -> SM ()   -- 상태를 변경
  16 updateSM f    =  SM (\s -> ((), f s)) 
  17 
  18 -- SM 모나드에서 계산 수행
  19 runSM              :: S -> SM a -> (a,S)
  20 runSM s0 (SM c)    =  c s0

이 예제는 새로운 타입 SM을 암묵적으로 타입 S를 끌고 다니는 계산으로서 정의합니다. 즉, 타입 SM t의 계산은 타입 t의 값을 정의하며, 타입 S의 상태와 상호작용(읽고 쓰기)을 합니다. SM의 정의는 간단합니다: 이것은 상태 하나를 받아서 두 개의 결과—(임의의 타입의) 리턴 값과 갱신된 상태—를 만드는 함수들로 이루어져 있습니다. 여기에서는 타입 동의어를 쓸 수 없으며, 인스턴스 선언에 사용될 SM 같은 이름이 필요합니다. 종종 data 대신 newtype 선언이 사용되기도 합니다.

이 인스턴스 선언은 모나드의 ‘배관 시설’을 정의합니다: 즉, 두 개의 계산과 빈(empty) 계산의 정의를 어떻게 순차화시키는지에 대해서 입니다. 순차화(sequencing; >>= 연산자)는 초기 상태 s0c1에 전달하고 이 계산으로부터 나오는 값 r을 두번째 계산 c2를 리턴하는 함수에 전달하는 (구성자 SM으로 표시되는) 계산을 정의합니다. 마지막으로, c1으로부터 나오는 상태는 c2에 전달되고 전체 결과는 c2의 결과입니다.

return의 정의는 더 쉽습니다: return은 상태를 전혀 바꾸지 않으며, 단지 값을 모나드에 가져다줄 뿐입니다.

>>=return이 기본적인 모나드 순차화 연산이긴 하지만, 몇 가지 모나드 원시함수(monadic primitives)도 역시 필요합니다. 모나드 원시함수는 단순히 모나드 추상의 내부를 사용하여 모나드가 동작하게끔 하는 ‘톱니바퀴’를 움직여주는 연산일 뿐입니다. 예를 들어, IO 모나드 내에서 putChar 같은 연산자들은 IO 모나드의 내부 작동을 다룬다는 면에서 원시함수입니다. 마찬가지로, 상기 상태 모나드는 readSMupdateSM이라는 두 개의 원시함수를 사용합니다. 이들은 모나드의 내부 구조에 의존적이라는 점에 주의하십시오. — SM 타입의 정의를 바꾸려면 이런 원시함수들도 바뀌어야 합니다.

readSMupdateSM의 정의는 단순합니다: readSM은 관측하기 위해서 상태를 모나드로부터 꺼내는 것이고 updateSM은 사용자로 하여금 모나드 속의 상태를 바꿀 수 있게 해 줍니다. (writeSM도 원시함수로서 사용할 수 있었지만, 상태를 다루는 데에 있어서는 대개 갱신이 더 자연스러운 방법입니다.)

마지막으로, 모나드 내에서 계산을 수행하는 함수 runSM이 필요합니다. 이 함수는 초기 상태와 계산을 입력받아서 계산 결과 값과 최종 상태를 모두 산출합니다.

좀 더 시야를 넓혀서 보자면, 우리가 하려는 것은 전체 계산들을 >>=과 return을 써서 순차화된 하나의 일련의 단계들(SM a 타입의 함수들)로 정의하려는 것입니다. 이런 단계들은 상태와 (readSM이나 updateSM을 통해) 상호작용하거나 혹은 상태를 무시할 수 있습니다. 그러나, 상태를 사용하는 (혹은 사용하지 않는) 것은 숨겨진 과정입니다: 계산들이 S를 사용하는가 사용하지 않는가에 따라 이 계산들의 시퀀스를 달리 유발시키지는 않습니다.

이 단순한 상태 모나드를 사용하는 예를 드는 대신, 상태 모나드를 포함하는 좀 더 복잡한 예제로 진행해보겠습니다. 자원을 소모하는 계산에 대한 작은 임베디드 언어를 정의합니다. 즉, Haskell 타입들과 함수들의 집합으로서 구현된 특수 목적의 언어를 만듭니다. 이런 언어들은 관심 분야에 특화된 연산과 타입의 라이브러리를 만들기 위해 기본적인 Haskell 도구들인 함수들과 타입들을 사용합니다.

이 예에서, 어떤 종류의 자원을 요구하는 계산을 고려해봅시다. 만일 자원을 사용할 수 있는 상황이라면 계산은 진행될 것이며, 자원이 사용 가능하지 않다면 계산은 보류됩니다. 우리가 만든 모나드에 의해 제어되는 자원을 사용하는 계산을 표시하기 위해서 R 타입을 사용하도록 합니다. R의 정의는 다음과 같습니다:

   1 data  R a    =  R (Resource -> (Resource, Either a (R a)))

각각의 계산은 사용 가능한 자원으로부터 사용 불가능한 자원으로의 함수이며, 사용 불가능한 자원은 a 타입의 결과값이나 R a 타입의 보류된 계산과 짝지어져 있고, 보류된 계산과 짝지어진 경우 자원이 고갈된 지점까지 수행된 작업을 캡쳐합니다.

R에 대한 Monad 인스턴스는 다음과 같습니다:

   1 instance  Monad R  where
   2     R c1 >>= fc2    =  R (\r -> case c1 r of
   3                                 (r', Left v)    -> let R c2 = fc2 v
   4                                                    in c2 r'
   5                                 (r', Right pc1) -> (r', Right (pc1 >>= fc2)))
   6     return v        =  R (\r -> (r, Left v))

Resource 타입은 상태 모나드에서의 상태와 같은 방식으로 사용됩니다. 이 정의는 다음과 같이 읽습니다: 두 개의 ‘자원이 충분한’ 계산들인 c1fc2 (c2를 만드는 함수)를 묶기 위해서는 초기 자원을 c1에 전달하라. 결과는 다음 두 가지 중 하나입니다.

보류는 반드시 두 번째 계산을 염두에 두어야만 합니다: pc1은 첫번째 계산인 c1만 보류시키므로, 전체 계산들을 보류하기 위해서는 반드시 여기에 c2를 바인딩시켜야 합니다. return의 정의는 v를 모나드 속에 집어넣는 동안 자원들은 변하지 않도록 내버려둡니다.

이 인스턴스 선언은 모나드의 기본 구조를 정의하지만 자원이 어떻게 사용될지를 결정하지는 않습니다. 이 모나드는 여러가지 타입의 자원을 제어하거나 여러 다른 종류의 자원 사용 정책을 구현하는 데에 사용될 수 있습니다. 예로서 아주 단순한 자원 정의를 하나 보여드리겠습니다: Resource를 사용가능한 계산 단계를 나타내는 Integer로 선택합니다:

   1 type  Resource    =  Integer

이 함수는 사용 가능한 단계가 전혀 남아있지 않은 경우만 아니라면 반드시 단계 하나를 받아들입니다:

   1 step      :: a -> R a
   2 step v    =  c  where
   3     c    =  R (\r -> if r /= 0 then (r-1, Left v)
   4                                else (r, Right c))

구성자 LeftRightEither 타입의 일부입니다. 이 함수는 최소한 하나의 계산 단계 자원이 사용가능하면 v를 리턴함으로써 R 내에서 계산을 계속합니다. 만일 한 단계도 가능하지 않다면, step 함수가 현재의 계산을 보류시키고 (이 보류는 c에 캡춰됩니다), 이 보류된 계산을 다시 모나드에 되돌려줍니다.

지금까지, 우리는 ‘자원을 사용할 수 있는 (resourceful)’ 일련의 계산들을 정의하는 도구(모나드)를 가지게 됐고, step을 사용해서 자원 사용의 형식을 표현할 수 있었습니다. 마지막으로, 이 모나드 내의 계산들이 어떻게 표현되는가를 알아볼 필요가 있습니다.

우리가 만든 모나드 내에 있는 증가 함수를 생각해봅시다:

   1 inc      :: R Integer -> R Integer
   2 inc i    =  do iValue <- i
   3                step (iValue + 1)

이것은 증가 함수를 단일 계산 단계로 정의하고 있습니다. <-는 인수 값을 모나드로부터 끄집어내기 위해서 필요합니다; iValue의 타입은 R Integer가 아니라 Integer입니다.

그럼에도 불구하고, 이 정의는 증가 함수의 표준적 정의에 비해 특별히 더 만족스럽지는 않습니다. 대신, + 처럼 이미 존재하는 연산을 ‘잘 가꿔서’ 이들을 우리의 모나드 세계속에서 작동하게끔 하는 방법은 없을까요? 몇 가지 Lifting 함수들로 시작해봅시다. 이것은 이미 존재하는 기능적 요소들을 모나드 내로 가져다줍니다. 다음과 같은 lift1의 정의를 생각해봅시다 (이것은 Monad 라이브러리에서 찾을 수 있는 liftM1과는 약간 다릅니다):

   1 lift1      :: (a -> b) -> (R a -> R b)
   2 lift1 f    =  \ra1 -> do a1 <- ra1
   3                          step (f a1)

이것은 인수 한 개를 받는 함수 f를 받아서, 한 단계로 lifted 함수를 수행시키는 함수를 R 속에 만듭니다. lift1을 사용하면 inc는 다음과 같아집니다:

   1 inc      :: R Integer -> R Integer
   2 inc i    =  lift1 (i + 1)

훨씬 나아지긴 했지만 아직도 이상적이진 않습니다. 먼저, lift2를 추가합니다:

   1 lift2      :: (a -> b -> c) -> (R a -> R b -> R c)
   2 lift2 f    =  \ra1 ra2 -> do a1 <- ra1
   3                              a2 <- ra2
   4                              step (f a1 a2)

이 함수는 lifted function 내에서의 평가 순서를 명시적으로 설정한다는 사실에 주목하십시오: a1을 산출하는 계산이 a2에 대한 계산보다 먼저 일어납니다.

lift2를 사용해서 모나드 R 내부에 새로운 버전의 ==를 만들어낼 수 있습니다:

   1 (==*)    :: Ord a => R a -> R a -> R Bool
   2 (==*)    =  lift2 (==)

==라는 이름이 이미 사용중이기 때문에 이 새로운 함수에는 조금 다른 이름을 사용해야 했었지만, 경우에 따라서는 lifted 함수와 unlifted 함수에 대해서 같은 이름을 사용할 수도 있습니다. 다음 인스턴스 선언은 Num 속의 모든 연산자들을 R에서 사용될 수 있게끔 해줍니다:

   1 instance  Num a => Num (R a)  where
   2     (+)            =  lift2 (+)
   3     (-)            =  lift2 (-)
   4     negate         =  lift1 negate
   5     (*)            =  lift2 (*)
   6     abs            =  lift1 abs
   7     fromInteger    =  return . fromInteger

함수 fromInteger는 Haskell 프로그램에서 모든 정수 상수에 암묵적으로 적용됩니다 (10.3절을 보시기 바랍니다); 이 정의로 인해서 정수 상수의 타입은 R Integer일 수 있게됩니다. 마침내, 이제 우리는 증가 함수를 완벽하게 자연스러운 스타일로 작성할 수 있습니다:

   1 inc      :: R Integer -> R Integer
   2 inc x    =  x + 1

클래스 Eq는 클래스 Num처럼 lift할 수 없다는 점에 주의하십시오: ==*의 결과는 Bool이 아니라 R Bool이기 때문에 ==*의 타입지정자는 ==의 허용가능한 오버로딩들과는 호환되지 않습니다.

R에서 흥미로운 계산들을 표현하기 위해서는 조건문이 필요해집니다. if는 사용할 수 없기 때문에 (조건 판정의 타입은 R Bool이 아니라 Bool이어야 합니다), 함수의 이름을 ifR이라고 붙이기로 합니다:

   1 ifR                :: R Bool -> R a -> R a -> R a
   2 ifR tst thn els    =  do t <- tst
   3                          if t then thn else els

이제는 모나드 R에서 더 큰 프로그램을 만들 수 있습니다:

   1 fact      :: R Integer -> R Integer
   2 fact x    =  ifR (x ==* 0) 1 (x * fact (x - 1))

이것은 평범한 팩토리얼 함수와 완전히 똑같지는 않지만 그래도 충분히 읽을만합니다. +if 같은 기존 연산에 새로운 정의를 내린다는 발상은 Haskell에서 임베디드 언어를 만드는 핵심입니다. 모나드는 이런 임베디드 언어의 의미론을 깔끔하고 모듈화된 방법으로 캡슐화하는 데에 특히 유용합니다.

이제 실제로 프로그램을 수행할 준비가 됐습니다. 이 함수는 계산 단계의 최대값을 받아서 M의 프로그램을 수행시킵니다:

   1 run            :: Resource -> R a -> Maybe a
   2 run s (R p)    =  case (p s) of 
   3                        (_, Left v) -> Just v
   4                         _          -> Nothing

할당된 수의 단계 안에 계산을 끝내지 못할 가능성을 다루기 위해 Maybe 타입을 사용합니다. 이제 다음과 같은 계산을 할 수 있습니다:

마지막으로, 이 모나드에 좀 더 흥미로운 기능들을 추가할 수 있습니다. 다음과 같은 함수를 생각해봅시다:

이것은 두 개의 계산을 병렬로 수행시키며, 먼저 완료된 쪽의 값을 리턴합니다. 이 함수의 정의로서 가능한 한 가지는 다음과 같습니다:

   1 c1 ||| c2    =  oneStep c1 (\c1' -> c2 ||| c1')  where
   2     oneStep             :: R a -> (R a -> R a) -> R a
   3     oneStep (R c1) f    =  R (\r -> case c1 1 of
   4                                          (r', Left v)    -> (r+r'-1, Left v)
   5                                          (r', Right c1') ->     -- r' must be 0
   6                                                let R next = f c1'
   7                                                in next (r+r'-1))

This takes a step in c1, returning its value of c1 complete or, if c1 returns a suspended computation (c1'), it evaluates c2 ||| c1'. The oneStep function takes a single step in its argument, either returning an evaluated value or passing the remainder of the computation into f. The definition of oneStep is simple: it gives c1 a 1 as its resource argument. If a final value is reached, this is returned, adjusting the returned step count (it is possible that a computation might return after taking no steps so the returned resource count isn't necessarily 0). If the computation suspends, a patched up resource count is passed to the final continuation.

We can now evaluate expressions like run 100 (fact (-1) ||| (fact 3)) without looping since the two calculations are interleaved. (Our definition of fact loops for -1). Many variations are possible on this basic structure. For example, we could extend the state to include a trace of the computation steps. We could also embed this monad inside the standard IO monad, allowing computations in M to interact with the outside world.

상기 예제가 이 튜토리얼에 나오는 다른 예제들보다는 조금 어려울지도 모르지만, 이것은 한 시스템의 기본적인 의미론을 정의하는 도구로서의 모나드의 힘을 보여주기 위한 예제입니다. 이 예제는 작은 Domain Specific Language의 모델이기도 하며, Haskell은 이런 언어를 정의하는 데에 특히 뛰어납니다. 여러가지 DSL들이 Haskell로 정의되어 있습니다; 이에 관한 더 많은 예들을 보시려면 haskell.org를 참조하십시오. 리액티브 애니메이션을 위한 언어인 Fran과 컴퓨터 음악용 언어인 Haskore 등이 있습니다.


이전 다음 위로
A Gentle Introduction to Haskell, Version 98

Copyright © 1999 Paul Hudak, John Peterson and Joseph Fasel

Permission is hereby granted, free of charge, to any person obtaining a copy of “A Gentle Introduction to Haskell” (the Text), to deal in the Text without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Text, and to permit persons to whom the Text is furnished to do so, subject to the following condition: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Text.