이성찬, 2017
Continuation 은 제어 흐름 을 구체화 해 first-class citizen으로 다룰 수 있게 한 추상 표현. Exception, Generator, Coroutine 등을 인코딩하는 데 유용한 도구. - wiki/Continuation
연산의 결과를 호출자에게 직접 되돌려주지 않고, 호출자가 인자로 넘겨준, continuation function 이라 불리는 함수에 결과값을 인자로 호출하는 스타일.
Continuation 이 first-class citizen이 아니지만, tail call optimization을 가진 함수형 언어에서 해당 연산들을 구현할 수 있다.
우리는 이를 통해 제어 흐름을 자유롭게 제어 할 수 있다.
설명이 필요 없는 간단한 예시
add :: Int -> Int -> Int
add x y = x + y
square x = x * x
pythagoras :: Int -> Int -> Int
pythagoras x y = add (square x) (square x)
thirteen = pythagoras 2 3
Continuation 함수를 매개변수로 받아, 연산의 결과를 인자로 호출.
# 단순한 CPS
add_cps :: Int
-> Int
-> (Int -> r) -- 주입 될 Cont. func.
-> r
add_cps x y = \c -> c $ x + y
square_cps x = \c -> c $ x * x
pythagoras_cps :: Int -> Int -> ((Int -> r) -> r)
pythagoras_cps x y = \c ->
square_cps x
$ \xsq -> square_cps y
$ \ysq -> add_cps xsq ysq c
thirteen_cps = pythagoras_cps 2 3 $ id
- 런타임이 갖고 있던 제어 흐름을 first-class 로 얻어오려다보니 그렇게 된 것.
- 이렇게까지 해서 얻는 그 힘이란게 무엇인가.
- 소개하기 전에, CPS가 하스켈에 왔으니 빼놓을 수 없는 ...
data Cont' r a = { runCont' :: (a -> r) -> r }
-- Functor, Applicative 생략
instance Monad (Cont' r) where
pure x = Cont' $ ($ x)
m >>= f = Cont' $ \c1 ->
runCont' m
$ \x -> runCont' (f x) c1
- f가 m에 continuation으로 주입된다는 사실을 주목.
- 그래서 m은 continuation을 통해 선형의 제어흐름을 벗어날 수 있는 주도권을 갖는다.
addM :: Int -> Int -> Cont' r Int
addM x y = pure $ x + y
squareM x = pure $ x * x
pythagorasM :: Int -> Int -> Cont' Int r
pythagorasM x y = do
xsq <- squareM x
ysq <- squareM y
addM xsq ysq
thirteenM = runCont' (pytagorasM 2 3) id
- monad를 구현하고, do-sugar를 이용해 한층 간결해졌다.
- CPS가 제어흐름을의 주도권을 갖는 강력한 예시를 보여줄 때가 되었다.
fizzbuzz :: Cont' [String] String
fizzbuzz = do
i <- (Cont' $ \fzbz -> catMaybes $ map fzbz [1..15])
num <- pure $ Just (show i)
fizz <- pure $ justWhen (i `mod` 3 == 0) "Fizz"
buzz <- pure $ justWhen (i `mod` 5 == 0) "Buzz"
pure $ fizz <> buzz <|> num
where justWhen b v = if b then Just v else Nothing
첫번째줄의 Cont'는 뒤따라올 statements들을 fzbz
란 이름의 continuation으로 넘겨받았지만, 앞선 pythagoras
예제처럼 continuation으로 tail call을 하지 않고, 몇번이고 호출하고 싶은 만큼 호출했다.
앞선 fizzbuzz를 continuation 주입이 일어남을 명시적으로 보여줌
fizzbuzz :: Cont' [String] String
fizzbuzz = do
(CPS $ \fzbz -> catMaybes $ map fzbz [1..15])
>>= \i -> do
num <- return $ Just (show i)
fizz <- return $ justWhen (i `mod` 3 == 0) "Fizz"
buzz <- return $ justWhen (i `mod` 5 == 0) "Buzz"
return $ fizz <> buzz <|> num)
where justWhen b v = if b then Just v else Nothing
callCC :: ((a -> Cont' r b) -> Cont' r a) -> Cont' r a
callCC f = Cont' $ \c2 ->
let exit y = Cont' $ const (c2 y)
in runCont' (f exit) c2
whatsYourName :: String -> String
whatsYourName name =
(`runCont'` id) $ do
res <- callCC $ \exit -> do -- (1)
when (null name) (exit "empty name") -- (2)
return $ "Welcome, " ++ name ++ "!" -- (3)
return res -- (4)
(2) 에서 (exit "empty name")
이 평가되면 (3) 은 반영되지 않고, callCC
구문은 CPS $ \_ -> "empty name"
으로 평가되어, "empty name"
을 리턴한다.
- callCC는 (4)를
exit
이란 이름의 continuation으로 받아, (2)에서 그 제어 흐름을 (4)로 옮길 수 있다고 해석하는것이 자연스럽다.
Monad 위에 ContT를 얹으면, ContT를 깔린 monad에 대한 callback 처럼 사용할 수 있고, 체이닝 할 수 있습니다.
data Hole = Swing Int | Attack Target
unitAttack :: Target -> ContT () IO Hole
unitAttack target = ContT $ \callback -> do
callback (Swing 60); callback (Attack target)
handler :: Hole -> ContT () IO String
handler (Swing n) = ContT $ \log -> do ... log $ "Swing " ++ show n
handler (Attack t) = ContT $ \log -> do ... log $ "Attack " ++ show t
stdoutLogger :: String -> ContT () IO ()
stdoutLogger text = ContT $ \_ -> putStrLn text
silentLogger text = ContT $ \_ -> pure ()
main =
runContT (unitAttack target
>>= handler
>>= silentLogger) return
다음처럼 하면 ContT로 모든 모나드를 구현할 수 있다고 한다.
i x = Cont' $ \k -> x >>= k
listM = (`runCont` return) $ do
a <- i [1, 2]
b <- i [10, 20]
return $ a + b
maybeM = (`runCont` return) $ do
a <- i $ Just "Fizz"
b <- i $ Just "Buzz"
return $ a <> b
i :: Monad m => m a
i x = Cont' $ \k -> x >>= k
-- free ride on Monad m
(i x) >>= f
-- suppose x :: m a, (>>=) in Monad (Cont' (m a))
== Cont' $ \k' -> runCont (i x)
$ \y -> runCont (f y) k'
== Cont' $ \k' -> runCont (Cont' $ \k -> x >>= k)
$ \y -> runCont (f y) k'
== Cont' $ \k' -> x >>= \y -> runCont (f y) k'
-- (>>=) in Monad m
i x
는 continuation k를 tail call 해서 Cont' do-block에 의해 주어진 순서 그대로, 제어 흐름을 변경하지 않고 (>>=) 했다.
- Toy VM 만드는 중.
- Esoteric programming language 비슷.
- ex: 아희, Brainfuck, etc.
- John Searle의 "중국어 방 논증"
- 우리는 튜링 테스트 피험자가 된다.
- ex: 명령어 Add에 대한 콜백으로
- auto:
do {a <- pop; b <- pop; push $ a+b}
- manual:
do {dump stack; hand_over_to_user; }
- auto:
- release는 manual 로 나가지만, 테스트는 auto 여야하니 ContT를 callback처럼 사용한다.
ContT로 Generator, Coroutine 구현하기. -> Iteratee, Pipe로 이어지나요?
- The Continuation Monad (haskellforall.com) monad transformer callback 예제 참고. CRVM과, 이 발표자료를 만들게 된 계기. http://www.haskellforall.com/2012/12/the-continuation-monad.html
- Haskell/Continuation Passing Style (en.wikibooks.org) Cont' 모나드 도입 참고했습니다. https://en.wikibooks.org/wiki/Haskell/Continuation_passing_style
- The Mother of all Monads 제곧내 http://blog.sigfpe.com/2008/12/mother-of-all-monads.html
- Control.Monad.Cont (mtl-2.2.1) callCC 예시, monad transformer callback 예시. https://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Cont.html
- Control.Monad.Trans.Cont (transformers-0.5.2.0) ContT 구현. https://hackage.haskell.org/package/transformers-0.5.2.0/docs/Control-Monad-Trans-Cont.html