연속
연속
연속(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) = #farg: any/c
프롬프트로 확장된 현재 연속으로 주어진 인자들에 proc을 적용해요. 프롬프트는 prompt-tag로 태그가 정해지는데, 이것은 default-continuation-prompt-tag(기본값)나 make-continuation-prompt-tag 중 하나의 결과여야 해요. call-with-continuation-prompt 호출은 proc의 결과를 돌려줘요.
handler 인자는, 설치된 프롬프트가 prompt-tag로 abort-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/cv: 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-continuation은 call-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)
연속이어야 하는 cont가 prompt-tag로 태그가 정해진 프롬프트를 포함하면 #t를, 그렇지 않으면 #f를 돌려줘요.
procedure
(continuation? v) → boolean?
v: any/c
v가 call-with-current-continuation, call-with-composable-continuation, call-with-escape-continuation이 만들어낸 연속이면 #t를, 그렇지 않으면 #f를 돌려줘요.
procedure
(continuation-prompt-tag? v) → boolean?
v: any/c
v가 default-continuation-prompt-tag나 make-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-thunk는 value-thunk이 돌려준 후에 호출돼요. dynamic-wind의 특별한 속성은 제어 흐름이 value-thunk 적용으로 들어가거나 나갈 때(프롬프트 abort나 연속 호출 때문에) 드러나요: 제어 흐름이 value-thunk 적용으로 들어갈 때마다 pre-thunk가 호출되고, value-thunk에서 나갈 때마다 post-thunk가 호출돼요. (pre-thunk와 post-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/base나 racket이 아니라 racket/control 라이브러리가 제공해요.
racket/control 라이브러리는 고차 제어 연산자에 관한 연구 문헌의 다양한 제어 연산자와, 몇 가지 추가 편의 형식을 제공해요. 이 제어 연산자들은 call-with-continuation-prompt, call-with-composable-continuation 등을 기반으로 구현되며, 일반적으로 잘 함께 동작해요. 그중 많은 것이 중복돼요. 예를 들어 reset과 prompt는 서로 바꿔 쓸 수 있어요.
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) = #farg: any/c
call/prompt 바인딩은 call-with-continuation-prompt의 별칭이에요.
procedure
(abort/cc prompt-tag v ...) → any
prompt-tag: any/cv: 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 ...+)
prompt와 control처럼 동작하지만, 특정 프롬프트 태그를 사용해요:
(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을 포함하지 않음
reset과 prompt 형식은 서로 바꿔 쓸 수 있어요.
syntax reset-at, shift-at
(reset-at prompt-tag-expr expr ...+)
(shift-at prompt-tag-expr identifier expr ...+)
reset과 shift처럼 동작하지만, 지정된 프롬프트 태그를 사용해요.
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])))
reset0와 prompt0 형식은 서로 바꿔 쓸 수 있어요. 게다가 다음 환원들이 적용돼요:
(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-prompt는 make-continuation-prompt-tag의 별칭이고, set은 prompt0-at의 별칭이며, cupto는 control0-at의 별칭이에요.