R6RS Scheme: 제어 기능
R6RS Scheme: 제어 기능
프로그램 실행 흐름을 특별한 방식으로 제어하는 기본 프로시저들을 설명해요. apply, 연속체를 포착하는 call/cc, 여러 값을 다루는 values·call-with-values, 그리고 동적 수명(dynamic extent) 전후에 프로시저를 실행하는 dynamic-wind를 다뤄요.
출처: 문서
본문
이 절은 프로그램 실행의 흐름을 특별한 방식으로 제어하는 다양한 기본 프로시저들을 설명해요.
(apply proc arg1 ... rest-args) — procedure
rest-args는 리스트여야 해요. proc은 n개의 인자를 받아들여야 하는데, 여기서 n은 args의 개수 더하기 rest-args의 길이예요. apply 프로시저는 목록 (append (list arg1 ...) rest-args)의 원소들을 실제 인자로 하여 proc을 호출해요.
apply에 대한 호출이 꼬리 맥락(tail context)에서 발생하면, proc에 대한 호출도 꼬리 맥락에 있어요.
(apply + (list 3 4)) ⇒ 7
(define compose
(lambda (f g)
(lambda args
(f (apply g args)))))
((compose sqrt *) 12 75) ⇒ 30
(call-with-current-continuation proc) — procedure
(call/cc proc) — procedure
proc은 하나의 인자를 받아들여야 해요. call-with-current-continuation 프로시저(그것은 call/cc 프로시저와 같아요)는 현재 연속체를 "탈출 프로시저(escape procedure)"로 묶어서 그것을 proc에 인자로 전달해요. 탈출 프로시저는 Scheme 프로시저인데, 나중에 호출되면 그 당시에 유효한 어떤 연속체도 버리고, 탈출 프로시저가 만들어졌을 때 유효했던 연속체를 대신 복원해요. 탈출 프로시저를 호출하면 dynamic-wind로 설치된 before·after 프로시저의 호출이 일어날 수 있어요.
탈출 프로시저는 원래의 call-with-current-continuation 호출의 연속체가 받아들이는 것과 같은 개수의 인자를 받아들여요.
proc에 전달되는 탈출 프로시저는 Scheme의 다른 어떤 프로시저와 마찬가지로 무한한 수명(extent)을 가져요. 그것은 변수나 데이터 구조에 저장되고 원하는 만큼 여러 번 호출될 수 있어요.
call-with-current-continuation에 대한 호출이 꼬리 맥락에서 발생하면, proc에 대한 호출도 꼬리 맥락에 있어요.
다음 예들은 call-with-current-continuation이 사용되는 일부 방식만 보여줘요. 실제 사용이 모두 이 예들처럼 단순했다면, call-with-current-continuation의 힘을 가진 프로시저가 필요하지 않았을 거예요.
(call-with-current-continuation
(lambda (exit)
(for-each (lambda (x)
(if (negative? x)
(exit x)))
'(54 0 37 -3 245 19))
#t)) ⇒ -3
(define list-length
(lambda (obj)
(call-with-current-continuation
(lambda (return)
(letrec ((r
(lambda (obj)
(cond ((null? obj) 0)
((pair? obj)
(+ (r (cdr obj)) 1))
(else (return #f))))))
(r obj))))))
(list-length '(1 2 3 4)) ⇒ 4
(list-length '(a b . c)) ⇒ #f
(call-with-current-continuation procedure?)
⇒ #t
Note: 탈출 프로시저를 호출하는 것은
call-with-current-continuation에 대한 호출의 동적 수명에 다시 진입하는 것이므로, 그 동적 환경을 복원해요. (5.12절 참조)
(values obj ...) — procedure
자신의 모든 인자를 자신의 연속체에 전달해요. values 프로시저는 다음과 같이 정의될 수 있어요:
(define (values . things)
(call-with-current-continuation
(lambda (cont) (apply cont things))))
lambda, begin, let, let*, letrec, letrec*, let-values, let*-values, case, cond 형식 같은 표현식 수열 안의 모든 비-최종 표현식의 연속체들은 보통 임의의 개수의 값을 받아들여요.
이러한 것들과 call-with-values, let-values, let*-values가 만드는 연속체를 제외하면, 단일 값을 암묵적으로 받아들이는 연속체 — 예컨대 프로시저 호출의 <operator>와 <operand>의 연속체 또는 조건문의 <test> 표현식의 연속체 — 는 정확히 하나의 값을 받아들여요. 그러한 연속체에 부적절한 개수의 값을 전달하는 효과는 미정의(undefined)예요.
(call-with-values producer consumer) — procedure
producer는 프로시저여야 하고 0개의 인자를 받아들여야 해요. consumer는 프로시저여야 하고 producer가 반환하는 만큼의 값을 받아들여야 해요. call-with-values 프로시저는 producer를 인자 없이 호출하는데, 그 때 어떤 값들이 전달되면 consumer 프로시저를 그 값들을 인자로 하여 호출하는 연속체를 만들어서 호출해요. consumer에 대한 호출의 연속체는 call-with-values에 대한 호출의 연속체예요.
(call-with-values (lambda () (values 4 5))
(lambda (a b) b))
⇒ 5
(call-with-values * -) ⇒ -1
call-with-values에 대한 호출이 꼬리 맥락에서 발생하면, consumer에 대한 호출도 꼬리 맥락에 있어요.
구현 책임:
producer가 반환한 후, 구현은consumer가producer가 반환한 만큼의 값을 받아들이는지 확인해야 해요.
(dynamic-wind before thunk after) — procedure
before, thunk, after는 프로시저여야 하고 각각 0개의 인자를 받아들여야 해요. 이 프로시저들은 임의의 개수의 값을 반환할 수 있어요. dynamic-wind 프로시저는 thunk를 인자 없이 호출하고, 이 호출의 결과들을 반환해요. 게다가 dynamic-wind는 thunk에 대한 호출의 동적 수명에 진입할 때마다 before를 인자 없이 호출하고, thunk에 대한 호출의 동적 수명에서 빠져나올 때마다 after를 인자 없이 호출해요. 따라서 call-with-current-continuation이 만든 탈출 프로시저에 대한 호출이 없을 때, dynamic-wind는 before, thunk, after를 그 순서대로 호출해요.
before와 after에 대한 호출은 thunk에 대한 호출의 동적 수명 안에 있는 것으로 간주되지 않지만, thunk에 대한 호출의 동적 수명 안에서 일어나는 다른 dynamic-wind 호출들의 before·after 프로시저에 대한 호출은 thunk에 대한 호출의 동적 수명 안에 있는 것으로 간주돼요.
더 정확히 말하면, 탈출 프로시저는 제어를 0개 이상의 활성 dynamic-wind 호출 x …의 동적 수명 밖으로 이동시키고, 0개 이상의 활성 dynamic-wind 호출 y …의 동적 수명 안으로 이동시켜요. 그것은 가장 최근의 x의 동적 수명을 떠나며 대응하는 after 프로시저를 인자 없이 호출해요. after 프로시저가 반환하면 탈출 프로시저는 그다음으로 최근인 x로 진행하고, 이런 식으로 계속돼요. 각 x가 이런 방식으로 처리되고 나면, 탈출 프로시저는 가장 먼(y)에 대응하는 before 프로시저를 인자 없이 호출해요. before 프로시저가 반환하면 탈출 프로시저는 가장 먼 y의 동적 수명에 다시 진입하고 그다음으로 먼 y로 진행하며, 이런 식으로 계속돼요. 각 y가 이런 방식으로 처리되고 나면, 제어는 탈출 프로시저에 담긴 연속체로 이동해요.
구현 책임: 구현은
thunk와after에 대한 제약을, 실제로 호출되는 경우에만 확인해야 해요.
(let ((path '())
(c #f))
(let ((add (lambda (s)
(set! path (cons s path)))))
(dynamic-wind
(lambda () (add 'connect))
(lambda ()
(add (call-with-current-continuation
(lambda (c0)
(set! c c0)
'talk1))))
(lambda () (add 'disconnect)))
(if (< (length path) 4)
(c 'talk2)
(reverse path))))
⇒ (connect talk1 disconnect connect talk2 disconnect)
(let ((n 0))
(call-with-current-continuation
(lambda (k)
(dynamic-wind
(lambda () (set! n (+ n 1)) (k))
(lambda () (set! n (+ n 2)))
(lambda () (set! n (+ n 4))))))
n) ⇒ 1
(let ((n 0))
(call-with-current-continuation
(lambda (k)
(dynamic-wind
values
(lambda ()
(dynamic-wind
values
(lambda ()
(set! n (+ n 1))
(k))
(lambda ()
(set! n (+ n 2))
(k))))
(lambda ()
(set! n (+ n 4))))))
n) ⇒ 7
Note: 동적 수명에 진입하는 것은 그 동적 환경을 복원해요. (5.12절 참조)