컨티뉴에이션 및 callcc
의 이해
컨티뉴에이션은 현재 평가 중인 식의 값을 받은 후 이어질 계산 전체를 의미하며, 루비의 callcc
는 이를 일급 값으로 캡처하고 재활용할 수 있게 합니다. 그러나 callcc
는 Ruby 2.2.2부터 Deprecated되었으며, 파이버(Fiber)와 달리 캡처된 컨티뉴에이션을 여러 번 호출할 수 있는 특성을 가집니다.
callcc
를 활용한 DSL 구현 및 한계
발표자는 하스켈(Haskell)의 리스트 모나드(List Monad)와 유사하게 백트래킹(backtracking)을 평탄하게 기술할 수 있는 DSL 구현 예시를 통해 callcc
의 유용성을 입증합니다. 일반적인 메타 프로그래밍 방식의 DSL 구현은 새로운 구문 추가 시 DSL 구현부를 계속 수정해야 하고 변수 관리가 복잡해지는 문제가 있습니다. 반면, callcc
를 사용하면 루비의 변수를 그대로 활용하며 백트래킹을 자연스럽게 구현할 수 있어 DSL 개발의 편의성이 크게 향상됩니다.
callcc
의 문제점
그럼에도 불구하고 callcc
는 다음과 같은 심각한 문제점을 내포합니다.
* 과도한 컨티뉴에이션 캡처: 필요한 부분뿐만 아니라 현재 실행 중인 전체 컨티뉴에이션을 캡처하여 불필요한 코드가 포함되고, callcc
호출 후 수동으로 복귀 지점을 지정해야 하는 복잡성을 야기합니다.
* 극심한 성능 저하: callcc
는 VM 내부 구현과 밀접하게 연관되어 있으며, 스택(stack) 전체를 memcpy
로 복사하고 setjmp
/longjmp
로 실행 위치를 기록/복원하는 과정에서 발생하는 오버헤드로 인해 성능이 매우 나쁩니다. 부분 스택 백업(sub-backup) 최적화가 적용되어 있지만, 여전히 비효율적입니다.
shift/reset
을 통한 문제 해결
shift/reset
은 shift
부터 reset
까지의 제한된 범위 내 컨티뉴에이션만 캡처하는 오퍼레이터입니다. 이는 DSL 구현을 훨씬 간결하게 만들며, callcc
와 달리 reset
지점으로 자동으로 복귀하므로 캡처된 컨티뉴에이션을 함수처럼 인수로 전달하여 사용할 수 있습니다. 놀랍게도 shift/reset
은 callcc
를 사용하여 구현할 수 있으며, 이러한 방식으로 구현된 shift/reset
을 DSL에 적용했을 때 callcc
단독 사용 시보다 성능이 크게 개선됨을 확인했습니다. 이는 shift/reset
의 구조가 callcc
의 스택 최적화와 더 잘 부합하기 때문으로 분석됩니다.