간략하게, Delimited continuation(구분?한정? 후속문)의 아이디어를 짚으면, 보통 Continuation Passing Style이라 할 때, 프로그램에서 현 시점 이후에 실행할 모든 작업이 후속문으로 잡히는 데, Delimited Continuation은 일정 범위까지만 후속문으로 잡을 수 있습니다.
Cont
모나드에는 callCC
외에 reset
, shift
구현이 있습니다. hoogle - Cont
reset :: Cont r r -> Cont r' r
-- reset m delimits the continuation of any shift inside m
shift :: ((a -> r) -> Cont r r) -> Cont r a
-- shift f captures the continuation up to the nearest enclosing reset and passes it to f
아래 코드는 Calvin’s Notebook - Delimited continuation에서 발췌했습니다.
1 + reset (2 * shift \k -> k (k 5))
^^^ ^^^ ^^^^^^^^^^^^^
k
는 후속문을 받을 자리인데, reset
다음부터 shift
이전까지를 후속문으로 받아 옵니다. 1+
를 제외한, reset
이 구분한 2*
까지만 후속문이 됩니다.
\k -> k (k 5)
가 현행문,
2 *
가 후속문,
보통 봐왔던 Continuation과 차이는 1+
는 후속문에 안들어갑니다.
reset
이하로 후속문 실행 작업이 끝나 (k
로 2 *
가 들어가) 2 * (2 * 5) = 20
이되고, 최종 결과는 20 + 1
해서 21
이 됩니다.
※ 왠 일로, 후속문을 받는 람다 헤드에 있는 변수는, 거의 모든 텍스트에서 \k
로 통일되어 있습니다. k
가 보이면, 그 자리로 후속문이 들어가겠거니 생각해도 됩니다.
CPS - Continuation Passing Style에서 callCC
코드를 뜯어 봤던 것처럼 reset
과 shift
를 뜯어보겠습니다.
callCC :: ((a -> Cont r b) -> Cont r a) -> Cont r a
^^^^^^^^^^^^^^^
shift
와 비슷한 고(고)차 함수 모양입니다. callCC
안에서 밑줄 친 서명 모양의 함수를 넣어줄 거라 예상할 수 있습니다.
runC :: Cont r r -> r
= runCont ct id runC ct
runC
가 꼬리로 id
를 붙여, 마치 더 이상 꼬리를 연결할 수 없도록 마감하는 역할을 합니다.
reset :: Cont a a -> Cont r a
CONT = Cont $ \k -> k (runC CONT) reset
runC
로 마감을 해 놓고는, 또 다시 꼬리를 붙일 수 있도록 k
를 받게끔 만들고 있습니다. 왜 이렇게 할까요?
shift :: ((a -> r) -> Cont r r) -> Cont r a
^^^^^^^mkCONT^^^^^^^
= Cont $ \k -> runC (mkCONT k) shift mkCONT
callCC
와 비슷하게 고(고)차 함수를 받습니다. 바인드가 다른 Cont
와 붙일 때, 꼬리를 넣어주는데, 이 걸 써먹는 모양의 함수입니다. 위 예시에서는 꼬리를 두 번 써먹는 k (k 5)
모양입니다. 물론 결과는 다시 Cont
액션인데, 여기서 또 runC
를 돌려 마감시킵니다. 어떻게 reset
, shift
조합이 흐름의 변화를 만들어내는지 이해가 안가 다음처럼 다 펼쳐놔 봤습니다.
1 + reset (2 * shift \k -> k (k 5))
Calvin 글에선 아래와 같이 liftM2
를 이용해 하스켈 Cont
모나드로 구현한 예시를 보여주는데, 전 bind
로 구현한 예시가 궁금했습니다.
example :: Cont Int Int
= liftM2 (+) (return 1) $
example *) (return 2)
reset (liftM2 (-> k <$> k <$> (return 5))))
(shift (\k
아래와 같이 bind
로 바꿨습니다.
example2 :: Cont Int Int
= do -- 바깥 컨텍스트(가)
example2 <- reset $ do -- 바깥과 구분된 내부 컨텍스트(나)
x <- return 2
y <- (shift (\k -> return $ k (k 5)))
z return $ y * z
return $ x + 1
일단, reset이 새 컨텍스트 (나)를 시작하는 건 눈에 잘 보입니다.
여기서 어떻게 y *
라는 continuation이 \k
로 들어가는지 알아보는 게 목표입니다.
newtype Cont r a = Cont ((a -> r) -> r)
runCont :: Cont r a -> (a -> r) -> r
-- Cont액션을 CONT라 표기하고, Cont액션을 만들어mk내는 걸 mkCont로 표기하겠습니다.
$ \c -> runCont CONT c
cont CONT >>= mkCONT = cont $ \c -> runCont CONT (\x -> runCont (mkCONT x) c)
CONT = Cont $ \rest -> rest (runC CONT)
reset = Cont $ \k -> runC (mkCONT k) shift mkCONT
reset
안 쪽 (나)만 따로 떼어서 보겠습니다.
do
<- return 2
y <- (shift (\k -> return $ k (k 5)))
z return $ y * z
>>=
가 눈에 보이게 하면,
return 2 >>= \y -> shift (\k -> return $ k (k 5)) >>= \z -> return y * z
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^(다)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^(라)
Cont
모나드의 바인드는 마치, 같은 모양의 레고 블럭을 끼우듯 연결합니다.
return 2
의 후속문은 (다)가 아니라, (라)입니다. ※ 모나드 3법칙으로 봐도 마찬가지입니다. \y...
문은 (다)가 아니라 (라)입니다.
(라)를 2
에 적용하니, \y
에 2
를 넣어 주면
-> return $ k (k 5)) >>= \z -> return 2 * z shift (\k
shift
를 풀어 보겠습니다.
-> return $ k (k 5)) >>= \z -> return 2 * z
shift (\k Cont $ \k' -> runC ((\k -> return $ k (k 5)) k') >>= \z -> return 2 * z
-- shift는 인자로 받은 Cont액션 안에 있는 값을 꺼내는 역할만 합니다.
-- 그 안에 있는 액션은 앞으로 들어 올 후속문 `k`에 따라 값이 정해집니다.
Cont $ \k' -> runC (return $ k' (k' 5)) >>= \z -> return 2 * z
Cont $ \k' -> k' (k' 5) >>= \z -> return 2 * z
‘>>=’ 동작으로 k'
에 (2 *)
를 넣어 주면, 2 * (2 * 5) = 20
이 됩니다.
정리하면, reset
으로 제한된 영역을 만들고, 그 안에서 또 shift
로 영역을 구분합니다. reset
도, shift
도 runC
로 끊은 후, 다시 Cont
로 만드는 이유입니다.
생각 스트레칭
... --> \y {\y1 ... --> \y2 ... --> \y3 ...} --> \z ... \x
바깥 컨텍스트 속에 있는 값과 관계 없이
\y{...}
만 경계를 두려면 어떻게 하면 될까요?
\y1, \y2, \y3
는 외부와 소통하는 유일한 길인y
와 관계없는 상태로 일단 만들어야 한다는 게 명확합니다.runC
가 하는 역할입니다.
@todo
useEffect보다 더 좋은 게 있다고? - YOKITOMI.log
Javascript의 코루틴을 쓰는, useEffect를 대체할 수 있는 useEff를 소개하는 글입니다.
@todo
@todo
Proposal - Delimited Continuation Primops로 올라와 있던 게 GHC 9.6.1에 프리미티브 기능으로 들어 갔다 합니다.
참고
Keynote: Delimited Continuations, Demystified by Alexis King | Lambda Days 2023
중급자들 볼만한 자료들을 쏟아내고 있는 Alexis King이, 눈높이를 최대한 낮춰줘서, 저는 볼 만한 영상이었습니다.