연속

연속

연속(continuation)은 Racket의 제어 흐름을 나타내는 핵심 개념이에요. 《Racket 가이드》의 연속 문서가 연속을 소개해요.

부표현식 평가 그리고 연속·구획 연속·배리어 문서에서 연속에 대한 일반적인 정보를 볼 수 있어요. 프롬프트와 조합 가능한 연속에 대한 Racket의 지원 [Flatt07]은 Sitaram의 %fcontrol 연산자 [Sitaram93]와 밀접하게 닮았어요.

출처: Racket Reference

본문

Racket은 다음 맥락에서 평가 주위에 연속 배리어(barrier)를 설치해서, 배리어로 보호된 평가 문맥으로의 완전 연속 점프를 막아요:

  • 예외 핸들러, 오류 탈출 핸들러, 오류 표시 핸들러를 적용할 때(예외 문서 참고);
  • 매크로 변환기(문법 변환기 문서 참고)를 적용하거나, 컴파일 타임 표현식을 평가하거나, 모듈 이름 리졸버를 적용할 때(모듈 이름 해석 문서 참고);
  • 사용자 정의 포트 프로시저(사용자 정의 포트 문서 참고), 이벤트 가드 프로시저(이벤트 문서 참고), 파라미터 가드 프로시저(파라미터 문서 참고)를 적용할 때;
  • 보안 가드 프로시저를 적용할 때(보안 가드 문서 참고);
  • 윌 프로시저를 적용할 때(윌과 실행자 문서 참고); 또는
  • 독립 실행형 Racket 명령줄에서 코드를 평가하거나 로드할 때(라켓 실행 또는 GRacket 문서 참고).

게다가 Racket의 확장이 추가 맥락에 배리어를 설치할 수도 있어요. 마지막으로 call-with-continuation-barrier는 적용과 현재 연속 사이에 thunk 배리어를 적용해요.

procedure

(call-with-continuation-prompt proc [prompt-tag handler] arg ...) → any
  • proc : procedure?
  • prompt-tag : continuation-prompt-tag? = (default-continuation-prompt-tag)
  • handler : (or/c procedure? #f) = #f
  • arg : any/c

프롬프트로 확장된 현재 연속으로 주어진 인자들에 proc을 적용해요. 프롬프트는 prompt-tag로 태그가 정해지는데, 이것은 default-continuation-prompt-tag(기본값)나 make-continuation-prompt-tag 중 하나의 결과여야 해요. call-with-continuation-prompt 호출은 proc의 결과를 돌려줘요.

handler 인자는, 설치된 프롬프트가 prompt-tagabort-current-continuation 호출의 대상이 될 때 call-with-continuation-prompt 호출에 대해 꼬리 위치에서 호출될 핸들러 프로시저를 지정해요. abort-current-continuation의 나머지 인자들이 핸들러 프로시저에 공급돼요. handler#f이면 기본 핸들러는 단일 abort-thunk 인자를 받고 (call-with-continuation-prompt abort-thunk prompt-tag #f)를 호출해요. 즉 기본 핸들러는 프롬프트를 다시 설치하고 주어진 thunk로 계속해요.

procedure

(abort-current-continuation prompt-tag v ...) → any
  • prompt-tag : any/c
  • v : any/c

현재 연속을 현재 연속에서 prompt-tag로 태그가 정해진 가장 가까운 프롬프트의 연속으로 재설정해요. 그런 프롬프트가 없으면 exn:fail:contract:continuation 예외가 발생해요. v들은 대상 프롬프트의 핸들러 프로시저에 인자로 전달돼요.

abort에 공급되는 v들에 대한 프로토콜은 prompt-tag에 특화돼요. abort-current-continuation(default-continuation-prompt-tag)와 함께 쓸 때는 일반적으로 기본 프롬프트 핸들러에 쓰기에 적합한 단일 thunk를 공급해야 해요. 비슷하게 call-with-continuation-prompt(default-continuation-prompt-tag)와 함께 쓸 때는 관련 핸들러가 일반적으로 단일 thunk 인자를 받아야 해요.

각 스레드의 연속은 (default-continuation-prompt-tag)에 대한 프롬프트로 시작하는데, 이 프롬프트는 (프롬프트를 보존한 채) 적용할 단일 thunk를 받는 기본 핸들러를 사용해요.

procedure

(make-continuation-prompt-tag) → continuation-prompt-tag?
(make-continuation-prompt-tag name) → continuation-prompt-tag?
  • name : symbol?

다른 어떤 값과도 equal?하지 않은 프롬프트 태그를 만들어요(make-continuation-prompt-tag의 이전·이후 결과 포함). 선택적 name 인자가 주어지면 출력이나 object-name을 위한 프롬프트 태그의 이름을 지정해요.

procedure

(default-continuation-prompt-tag) → continuation-prompt-tag?

모든 스레드의 연속 시작 부분에 프롬프트가 설치되는 상수 프롬프트 태그를 돌려줘요. 각 스레드의 초기 프롬프트 핸들러는 임의 개수의 값을 받아서 돌려줘요. default-continuation-prompt-tag의 결과는 프롬프트 태그를 받는 모든 프로시저의 기본 태그예요.

procedure

(call-with-current-continuation proc [prompt-tag]) → any
  • proc : (continuation? . -> . any)
  • prompt-tag : continuation-prompt-tag? = (default-continuation-prompt-tag)

prompt-tag로 태그가 정해진 가장 가까운 프롬프트까지 현재 연속을 포착해요. 그런 프롬프트가 없으면 exn:fail:contract:continuation 예외가 발생해요. 잘려진 연속은 프롬프트 이후 설치된 연속 마크와 dynamic-wind 프레임만 포함해요.

포착된 연속은 call-with-current-continuation 호출에 대해 꼬리 위치에서 호출되는 proc에 전달돼요.

proc에 주어지는 연속 인자가 적용되면, prompt-tag로 태그가 정해진 가장 가까운 프롬프트(프롬프트는 포함하지 않음; 그런 프롬프트가 없으면 exn:fail:contract:continuation 예외 발생)까지, 또는 현재 연속과 포착된 연속이 공유하는 가장 가까운 연속 프레임(있는 경우)—둘 중 먼저인 쪽까지—현재 연속의 해당 부분을 제거해요. 연속 프레임을 제거하면서 dynamic-wind의 후행 thunk(post-thunk)가 실행돼요. 마지막으로 (공유되지 않은 부분의) 포착된 연속이 남은 연속에 덧붙여지는데, dynamic-wind의 선행 thunk(pre-thunk)를 적용해요.

적용된 연속 프로시저에 공급된 인자들은 복원된 연속의 결과 값이 돼요. 특히 여러 인자가 공급되면 연속은 여러 결과를 받아요.

적용 시점에 현재 연속을 적용된 연속으로 교체하면 연속 배리어가 도입될 경우 exn:fail:contract:continuation 예외가 발생해요.

연속은 그것이 포착된 스레드(스레드 문서 참고)가 아닌 다른 스레드에서 호출될 수 있어요.

procedure

(call/cc proc [prompt-tag]) → any
  • proc : (continuation? . -> . any)
  • prompt-tag : continuation-prompt-tag? = (default-continuation-prompt-tag)

call/cc 바인딩은 call-with-current-continuation의 별칭이에요.

procedure

(call-with-composable-continuation proc [prompt-tag]) → any
  • proc : (continuation? . -> . any)
  • prompt-tag : continuation-prompt-tag? = (default-continuation-prompt-tag)

call-with-current-continuation과 비슷하지만, 결과 연속 프로시저를 적용할 때 현재 연속의 어떤 부분도 제거하지 않아요. 대신 적용은 항상 현재 연속을 포착된 연속으로 확장해요(연속에 포착된 프롬프트 외에 어떤 프롬프트도 설치하지 않고).

call-with-composable-continuation이 호출될 때, prompt-tag로 태그가 정해진 가장 가까운 프롬프트보다 앞에 연속 배리어가 나타나면 exn:fail:contract:continuation 예외가 발생해요(연속을 적용하려는 시도가 항상 실패할 것이기 때문).

procedure

(call-with-escape-continuation proc) → any
  • proc : (continuation? . -> . any)

call-with-current-continuation과 비슷하지만, proc이 꼬리 위치에서 호출되지 않고, proc에 공급되는 연속 프로시저는 call-with-escape-continuation 호출의 동적 범위 동안에만 호출될 수 있어요.

call-with-escape-continuation에서 얻은 연속은 실제로 일종의 프롬프트예요. 탈출 연속은 Racket의 일반 프롬프트보다 먼저 나온 것이므로 주로 하위 호환성을 위해 제공돼요. Racket의 BC 구현에서 call-with-escape-continuationcall-with-current-continuation보다 더 효율적으로 구현되므로, 그런 오래된 Racket 변형에서 성능을 개선하려고 call-with-current-continuation 대신 call-with-escape-continuation을 쓸 때가 있어요.

procedure

(call/ec proc) → any
  • proc : (continuation? . -> . any)

call/ec 바인딩은 call-with-escape-continuation의 별칭이에요.

procedure

(call-in-continuation k proc) → any
  • k : continuation?
  • proc : (-> any)

연속 k를 적용하는 것과 비슷하지만, 값을 연속에 전달하는 대신 k를 호출의 연속으로 해서 proc을 호출해요(그래서 proc의 결과가 연속에 돌려져요). k가 조합 가능한 연속이면, proc 호출의 연속은 k로 확장된 현재 연속이에요.

예시:

> (+ 1
     (call/cc (lambda (k)
                (call-in-continuation k (lambda () 4)))))
5
> (+ 1
     (call/cc (lambda (k)
                (let ([n 0])
                  (dynamic-wind
                   void
                   (lambda ()
                     ; n은 후행 thunk 이후에 접근
                     (call-in-continuation k (lambda () n)))
                   (lambda ()
                     (set! n 4)))))))
5
> (+ 1
     (with-continuation-mark
      'n 4
      (call/cc (lambda (k)
                  (with-continuation-mark
                   'n 0
                   (call-in-continuation
                    k
                    (lambda ()
                      ; 연속에서 'n 마크 접근
                      (continuation-mark-set-first #f 'n))))))))
5

syntax let/cc

(let/cc k body ...+)

(call/cc (lambda (k) body ...))와 동등해요.

syntax let/ec

(let/ec k body ...+)

(call/ec (lambda (k) body ...))와 동등해요.

procedure

(call-with-continuation-barrier thunk) → any
  • thunk : (-> any)

적용과 현재 연속 사이에 연속 배리어를 두고 thunk를 적용해요. thunk의 결과는 call-with-continuation-barrier 호출의 결과예요.

procedure

(continuation-prompt-available? prompt-tag [cont]) → any
  • prompt-tag : continuation-prompt-tag?
  • cont : continuation? = (call/cc values)

연속이어야 하는 contprompt-tag로 태그가 정해진 프롬프트를 포함하면 #t를, 그렇지 않으면 #f를 돌려줘요.

procedure

(continuation? v) → boolean?
  • v : any/c

vcall-with-current-continuation, call-with-composable-continuation, call-with-escape-continuation이 만들어낸 연속이면 #t를, 그렇지 않으면 #f를 돌려줘요.

procedure

(continuation-prompt-tag? v) → boolean?
  • v : any/c

vdefault-continuation-prompt-tagmake-continuation-prompt-tag가 만들어낸 연속 프롬프트 태그이면 #t를 돌려줘요.

procedure

(dynamic-wind pre-thunk value-thunk post-thunk) → any
  • pre-thunk : (-> any)
  • value-thunk : (-> any)
  • post-thunk : (-> any)

세 개의 thunk 인자를 순서대로 적용해요. dynamic-wind 표현식의 값은 value-thunk이 돌려주는 값이에요. pre-thunk 프로시저는 value-thunk을 호출하기 전에 호출되고, post-thunkvalue-thunk이 돌려준 후에 호출돼요. dynamic-wind의 특별한 속성은 제어 흐름이 value-thunk 적용으로 들어가거나 나갈 때(프롬프트 abort나 연속 호출 때문에) 드러나요: 제어 흐름이 value-thunk 적용으로 들어갈 때마다 pre-thunk가 호출되고, value-thunk에서 나갈 때마다 post-thunk가 호출돼요. (pre-thunkpost-thunk 적용으로의 점프나 그로부터의 점프에는 특별한 처리가 수행되지 않아요.)

dynamic-wind가 정상 평가를 위해 pre-thunk을 호출할 때, pre-thunk 적용의 연속은 (dynamic-wind의 특별한 점프 처리를 갖고) value-thunk을 호출한 다음 post-thunk를 호출해요. 비슷하게 post-thunk 적용의 연속은 앞선 value-thunk 적용의 값을 전체 dynamic-wind 적용의 연속에 돌려줘요.

pre-thunk이 연속 점프 때문에 호출될 때, pre-thunk 호출의 연속은

  • 더 깊이 중첩된 pre-thunk가 있으면 그쪽으로 점프하거나, 목적지 연속으로 점프하고; 그런 다음
  • 목적지 연속에서 둘러싸는 dynamic-wind 호출과 동일하게 계속해요(즉, 포착을 구획지은 둘러싸는 프롬프트까지 원래 dynamic-wind 호출의 연속과 일치).

보통 이 연속의 두 번째 부분은 첫 번째 부분의 점프 때문에 도달하지 않아요. 하지만 두 번째 부분은 중요한데, 목적지 연속 안에 있는 dynamic-wind 호출의 연속에 포함된 탈출 연속으로의 점프를 가능하게 하기 때문이에요. 게다가 pre-thunk에 대한 연속 마크(연속 마크 문서 참고)와 파라미터화(파라미터 문서 참고)가 둘러싸는 dynamic-wind 호출의 것과 일치한다는 것을 뜻해요. 다만 pre-thunk 호출은 브레이크를 비활성화하도록 parameterize-break 처리돼요(브레이크 문서 참고).

마찬가지로 post-thunk가 연속 점프 때문에 호출될 때, post-thunk 호출의 연속은 덜 깊게 중첩된 post-thunk가 있으면 그쪽으로 점프하거나, 목적지를 보호하는 pre-thunk가 있으면 그쪽으로 점프하거나, 목적지 연속으로 점프한 다음, 점프의 원래 연속 안에서 둘러싸는 dynamic-wind 호출과 동일하게 계속해요. pre-thunk에서와 마찬가지로 dynamic-wind 호출의 연속 마크와 파라미터화는 post-thunk에도 적용되는데, 다만 호출이 브레이크를 비활성화하도록 추가로 parameterize-break 처리된다는 점이 달라요.

두 경우 모두 점프의 목적지는 각 pre-thunk 또는 post-thunk가 완료된 후 다시 계산돼요. 프롬프트-구획 연속(프롬프트·구획 연속·배리어 문서 참고)이 post-thunk 안에서 포착되면, 그것은 점프의 목적지가 연속 적용 시점과 연속 포착 시점에서 달라지도록 구획되고 인스턴스화될 수 있어요. 복원 후 관련 프롬프트나 탈출 연속이 연속에 없으면 적절한 목적지가 전혀 없을 수도 있는데, 그 경우 pre-thunk이나 post-thunk 연속의 첫 단계가 예외를 발생시킬 수 있어요.

예시:

> (let ([v (let/ec out
             (dynamic-wind
              (lambda () (display "in "))
              (lambda ()
                (display "pre ")
                (display (call/cc out))
                #f)
              (lambda () (display "out "))))])
    (when v (v "post ")))
in pre out in post out 
> (let/ec k0
    (let/ec k1
      (dynamic-wind
       void
       (lambda () (k0 'cancel))
       (lambda () (k1 'cancel-canceled)))))
'cancel-canceled
> (let* ([x (make-parameter 0)]
         [l null]
         [add (lambda (a b)
                (set! l (append l (list (cons a b)))))])
    (let ([k (parameterize ([x 5])
               (dynamic-wind
                   (lambda () (add 1 (x)))
                   (lambda () (parameterize ([x 6])
                                (let ([k+e (let/cc k (cons k void))])
                                  (add 2 (x))
                                  ((cdr k+e))
                                  (car k+e))))
                   (lambda () (add 3 (x)))))])
      (parameterize ([x 7])
        (let/cc esc
          (k (cons void esc)))))
    l)
'((1 . 5) (2 . 6) (3 . 5) (1 . 5) (2 . 6) (3 . 5))

추가 제어 연산자

  • (require racket/control)package: base

이 절에 문서화된 바인딩은 racket/baseracket이 아니라 racket/control 라이브러리가 제공해요.

racket/control 라이브러리는 고차 제어 연산자에 관한 연구 문헌의 다양한 제어 연산자와, 몇 가지 추가 편의 형식을 제공해요. 이 제어 연산자들은 call-with-continuation-prompt, call-with-composable-continuation 등을 기반으로 구현되며, 일반적으로 잘 함께 동작해요. 그중 많은 것이 중복돼요. 예를 들어 resetprompt는 서로 바꿔 쓸 수 있어요.

procedure

(call/prompt proc [prompt-tag handler] arg ...) → any
  • proc : procedure?
  • prompt-tag : continuation-prompt-tag? = (default-continuation-prompt-tag)
  • handler : (or/c procedure? #f) = #f
  • arg : any/c

call/prompt 바인딩은 call-with-continuation-prompt의 별칭이에요.

procedure

(abort/cc prompt-tag v ...) → any
  • prompt-tag : any/c
  • v : any/c

abort/cc 바인딩은 abort-current-continuation의 별칭이에요.

procedure

(call/comp proc [prompt-tag]) → any
  • proc : (continuation? . -> . any)
  • prompt-tag : continuation-prompt-tag? = (default-continuation-prompt-tag)

call/comp 바인딩은 call-with-composable-continuation의 별칭이에요.

procedure

(abort v ...) → any
  • v : any/c

기본 연속 프롬프트 태그와 기본 abort 핸들러를 사용해 v들을 프롬프트에 돌려줘요. 즉 (abort v ...)

(abort-current-continuation
 (default-continuation-prompt-tag)
 (lambda () (values v ...)))

와 동등해요.

예시:

> (prompt
    (printf "start here\n")
    (printf "answer is ~a\n" (+ 2 (abort 3))))
start here
3

syntax %

(% expr)
(% expr handler-expr)
(% expr handler-expr #:tag tag-expr)

fcontrol

procedure
(fcontrol v #:tag prompt-tag) → any
v : any/c
prompt-tag : (default-continuation-prompt-tag)

Sitaram의 연산자들이에요 [Sitaram93]. 핵심 환원 규칙은:

(% val proc) => val
(% E[(fcontrol val)] proc) => (proc val (lambda (x) E[x]))
; 여기서 E는 %를 포함하지 않음

handler-expr가 생략되면 %prompt와 같아요. prompt-tag가 제공되면 %prompt-at처럼 특정 프롬프트 태그를 사용해요.

예시:

> (% (+ 2 (fcontrol 5))
     (lambda (v k)
       (k v)))
7
> (% (+ 2 (fcontrol 5))
     (lambda (v k)
       v))
5

syntax prompt, control

(prompt expr ...+)
(control id expr ...+)

고차 제어를 위한 가장 초기의 연산자 중 하나예요 [Felleisen88a, Felleisen88, Sitaram90]. 핵심 환원 규칙은:

(prompt val) => val
(prompt E[(control k expr)]) => (prompt ((lambda (k) expr)
                                          (lambda (v) E[v])))
; 여기서 E는 prompt를 포함하지 않음

예시:

> (prompt
    (+ 2 (control k (k 5))))
7
> (prompt
    (+ 2 (control k 5)))
5
> (prompt
    (+ 2 (control k (+ 1 (control k1 (k1 6))))))
7
> (prompt
    (+ 2 (control k (+ 1 (control k1 (k 6))))))
8
> (prompt
    (+ 2 (control k (control k1 (control k2 (k2 6))))))
6

syntax prompt-at, control-at

(prompt-at prompt-tag-expr expr ...+)
(control-at prompt-tag-expr id expr ...+)

promptcontrol처럼 동작하지만, 특정 프롬프트 태그를 사용해요:

(prompt-at tag val) => val
(prompt-at tag E[(control-at tag k expr)]) => (prompt-at tag
((lambda (k) expr)
(lambda (v) E[v])))
; 여기서 E는 tag에 대한 prompt-at을 포함하지 않음

syntax reset, shift

(reset expr ...+)
(shift id expr ...+)

Danvy와 Filinski의 연산자들이에요 [Danvy90]. 핵심 환원 규칙은:

(reset val) => val
(reset E[(shift k expr)]) => (reset ((lambda (k) expr)
(lambda (v) (reset E[v]))))
; 여기서 E는 reset을 포함하지 않음

resetprompt 형식은 서로 바꿔 쓸 수 있어요.

syntax reset-at, shift-at

(reset-at prompt-tag-expr expr ...+)
(shift-at prompt-tag-expr identifier expr ...+)

resetshift처럼 동작하지만, 지정된 프롬프트 태그를 사용해요.

syntax prompt0, reset0, control0, shift0

(prompt0 expr ...+)
(reset0 expr ...+)
(control0 id expr ...+)
(shift0 id expr ...+)

prompt 등의 일반화예요 [Shan04]. 핵심 환원 규칙은:

(prompt0 val) => val
(prompt0 E[(control0 k expr)]) => ((lambda (k) expr)
(lambda (v) E[v]))
(reset0 val) => val
(reset0 E[(shift0 k expr)]) => ((lambda (k) expr)
(lambda (v) (reset0 E[v])))

reset0prompt0 형식은 서로 바꿔 쓸 수 있어요. 게다가 다음 환원들이 적용돼요:

(prompt E[(control0 k expr)]) => (prompt ((lambda (k) expr)
(lambda (v) E[v])))
(reset E[(shift0 k expr)]) => (reset ((lambda (k) expr)
(lambda (v) (reset0 E[v]))))
(prompt0 E[(control k expr)]) => (prompt0 ((lambda (k) expr)
(lambda (v) E[v])))
(reset0 E[(shift k expr)]) => (reset0 ((lambda (k) expr)
(lambda (v) (reset E[v]))))

즉, 0-스타일 동작을 위해선 prompt/reset 위치와 control/shift 위치가 모두 일치해야 하고, 그렇지 않으면 0이 아닌 동작이 적용돼요.

syntax prompt0-at, reset0-at, control0-at, shift0-at

(prompt0-at prompt-tag-expr expr ...+)
(reset0-at prompt-tag-expr expr ...+)
(control0-at prompt-tag-expr id expr ...+)
(shift0-at prompt-tag-expr id expr ...+)

프롬프트 태그를 받아들이는 prompt0 등의 변형이에요.

procedure

(spawn proc) → any
  • proc : ((any/c . -> . any) . -> . any)

Hieb와 Dybvig의 연산자들이에요 [Hieb90]. 핵심 환원 규칙은:

(spawn proc)
=> (prompt/spawn tag
   (proc (lambda (proc) (abort/spawn tag proc))))
; 여기서 tag는 갓 생성된 프롬프트 태그
(prompt/spawn tag val)
=> val
(prompt/spawn tag E[(abort/spawn tag proc)])
=> (proc (lambda (x) (prompt/spawn tag E[x])))
; 여기서 E는 tag에 대한 prompt/spawn을 포함하지 않음

procedure

(splitter proc) → any
  • proc : (((-> any) . -> . any) ((continuation? . -> . any) . -> . any) . -> . any)

Queinnec와 Serpette의 연산자예요 [Queinnec91]. 핵심 환원 규칙은:

(splitter proc)
=> (prompt/splitter tag
   (proc (lambda (thunk) (abort/splitter tag thunk))
         (lambda (proc) (control0/splitter tag k (proc k)))))
; 여기서 tag는 갓 생성된 프롬프트 태그
(prompt/splitter tag val)
=> val
(prompt/splitter tag E[(abort/splitter tag thunk)])
=> (thunk)
; 여기서 E는 tag에 대한 prompt/splitter를 포함하지 않음
(prompt/splitter tag E[(control0/splitter tag k expr)])
=> ((lambda (k) expr)
   (lambda (x) E[x]))
; 여기서 E는 tag에 대한 prompt/splitter를 포함하지 않음

procedure

(new-prompt) → continuation-prompt-tag?
(new-prompt name) → continuation-prompt-tag?
  • name : symbol?

syntax set, cupto

(set prompt-expr expr ...+)
(cupto prompt-expr id expr ...+)

Gunter 등의 연산자들이에요 [Gunter95]. 이 라이브러리에서 new-promptmake-continuation-prompt-tag의 별칭이고, setprompt0-at의 별칭이며, cuptocontrol0-at의 별칭이에요.

더 알아보기