RESTART-CASE — 리스타트를 선언적으로 설치하는 매크로
RESTART-CASE — 리스타트를 선언적으로 설치하는 매크로
restart-bind를 좀 더 쓰기 편하게 다듬은 상위 매크로예요. 리스타트할 지점을 이름과 lambda list, 옵션으로 선언하면, 몸체에서 조건으로 제어를 넘겨받는 구조를 쉽게 만들 수 있어요.
시그니처
restart-case restartable-form {clause}* => results*
clause::= (case-name lambda-list
[[:interactive interactive-expression | :report report-expression | :test test-expression]]
declaration* form*)
본문
인자와 값 (Arguments and Values)
restartable-form— 폼(form).case-name— 심볼 또는nil.lambda-list— 보통 lambda list(ordinary lambda list).interactive-expression— 심볼 또는 lambda 식.report-expression— 문자열, 심볼, 또는 lambda 식.test-expression— 심볼 또는 lambda 식.declaration—declare식; 평가되지 않아요.form— 폼.results—restartable-form의 평가로 얻은 값, 또는 선택된 절에서 실행된 마지막form이 돌려준 값, 또는nil.
설명 (Description)
restart-case는 절들(clauses)이 제어가 옮겨질 특별한 지점으로 의미를 갖는 동적 환경에서 restartable-form을 평가해요. restartable-form이 끝까지 실행되고 값을 돌려주면, 그 값이 restart-case가 돌려주는 값이 되고 처리가 끝나요. restartable-form 실행 중에는 어떤 코드든 invoke-restart를 통해 절들 중 하나로 제어를 옮길 수 있어요. 이동이 일어나면 그 절 몸체의 폼들이 평가되고, 마지막 폼이 돌려준 값이 restart-case의 값이 돼요. 이 경우 절 실행 전에 restartable-form 주위에 설치된 리스타트들이 더 이상 활성화되지 않도록 동적 상태가 적절히 풀려요(unwind).
선택된 절에 form이 하나도 없으면 restart-case는 nil을 돌려줘요.
case-name이 심볼이면 그 리스타트의 이름이 돼요.
여러 절이 같은 case-name을 쓸 수 있어요. 그 경우 find-restart는 그 이름을 가진 첫 번째 절을 찾아요. 나머지 절들은 compute-restarts로 접근할 수 있어요.
각 arglist는 해당 절의 form들을 실행하는 동안 바인딩할 보통 lambda list예요. 이 매개변수들은 invoke-restart 호출에서 필요한 데이터를 받기 위해 쓰여요.
기본적으로 invoke-restart-interactively는 인자를 전달하지 않으므로, 대화형 리스타팅을 수용하려면 모든 인자가 선택적이어야 해요. 하지만 :interactive 키워드로 invoke-restart-interactively에 적절한 인자 목록 계산법을 알려주면 인자를 선택적으로 두지 않아도 돼요.
키워드 옵션들은 이런 의미를 가져요.
- :interactive —
:interactive value값은function에 적합한 인자여야 해요.(function value)가 현재 어휘 환경에서 평가돼, 인자 없이invoke-restart-interactively가 쓸 인자들을 돌려주는 함수를 반환해야 해요.invoke-restart-interactively는 리스타트 시도 전의 동적 환경에서 호출되고, 사용자 상호작용에 query I/O를 써요. 리스타트가 대화형으로 호출되는데:interactive옵션이 없으면 호출에서 쓰는 인자 목록은 빈 목록이에요. - :report —
:report value값이 lambda 식이나 심볼이면function에 받아들여져야 해요.(function value)가 현재 어휘 환경에서 평가돼, 인자 하나(스트림)를 받아 그 스트림에 리스타트 설명을 출력하는 함수를 반환해야 해요.*print-escape*가nil일 때 리스타트가 출력될 때마다 호출돼요. 값이 문자열이면 다음 축약형이에요:
이름이 붙은 리스타트가 보고를 요청받았는데 보고 정보가 없으면, 기본 보고 문구를 만들 때 리스타트 이름이 쓰여요.(lambda (stream) (write-string value stream))*print-escape*가nil일 때 프린터는 리스타트에 보고 정보를 사용해요. 예를 들어 디버거가:continue명령 입력으로 이런 행동을 안내할 수 있어요:
그러면 대략 이렇게 표시돼요:(format t "~&~S -- ~A~%" ':continue some-restart)
익명 리스타트에:CONTINUE -- Return to command level:report옵션 없이 지정하면 결과는 명시되지 않아요(unspecified). - :test —
:test value값은function에 적합한 인자여야 해요.(function value)가 현재 어휘 환경에서 평가돼, 인자 하나(조건)를 받아 그 리스타트가 보일지 여부를 true/false로 돌려주는 함수를 반환해야 해요. 기본값은(lambda (c) (declare (ignore c)) t)와 동등해요.
restartable-form이 car가 signal, error, cerror, warn 중 하나인 목록이거나(또는 그런 목록으로 매크로확장되는 매크로 폼이면), 표시될 조건에 지정된 리스타트들을 연결하기 위해 with-condition-restarts가 암묵적으로 사용돼요.
예제 (Examples)
(restart-case
(handler-bind ((error #'(lambda (c)
(declare (ignore condition))
(invoke-restart 'my-restart 7))))
(error "Foo."))
(my-restart (&optional v) v))
=> 7
(define-condition food-error (error) ())
=> FOOD-ERROR
(define-condition bad-tasting-sundae (food-error)
((ice-cream :initarg :ice-cream :reader bad-tasting-sundae-ice-cream)
(sauce :initarg :sauce :reader bad-tasting-sundae-sauce)
(topping :initarg :topping :reader bad-tasting-sundae-topping))
(:report (lambda (condition stream)
(format stream "Bad tasting sundae with ~S, ~S, and ~S"
(bad-tasting-sundae-ice-cream condition)
(bad-tasting-sundae-sauce condition)
(bad-tasting-sundae-topping condition)))))
=> BAD-TASTING-SUNDAE
(defun all-start-with-same-letter (symbol1 symbol2 symbol3)
(let ((first-letter (char (symbol-name symbol1) 0)))
(and (eql first-letter (char (symbol-name symbol2) 0))
(eql first-letter (char (symbol-name symbol3) 0)))))
=> ALL-START-WITH-SAME-LETTER
(defun read-new-value ()
(format t "Enter a new value: ")
(multiple-value-list (eval (read))))
=> READ-NEW-VALUE
(defun verify-or-fix-perfect-sundae (ice-cream sauce topping)
(do ()
((all-start-with-same-letter ice-cream sauce topping))
(restart-case
(error 'bad-tasting-sundae
:ice-cream ice-cream
:sauce sauce
:topping topping)
(use-new-ice-cream (new-ice-cream)
:report "Use a new ice cream."
:interactive read-new-value
(setq ice-cream new-ice-cream))
(use-new-sauce (new-sauce)
:report "Use a new sauce."
:interactive read-new-value
(setq sauce new-sauce))
(use-new-topping (new-topping)
:report "Use a new topping."
:interactive read-new-value
(setq topping new-topping))))
(values ice-cream sauce topping))
=> VERIFY-OR-FIX-PERFECT-SUNDAE
(verify-or-fix-perfect-sundae 'vanilla 'caramel 'cherry)
>> Error: Bad tasting sundae with VANILLA, CARAMEL, and CHERRY.
>> To continue, type :CONTINUE followed by an option number:
>> 1: Use a new ice cream.
>> 2: Use a new sauce.
>> 3: Use a new topping.
>> 4: Return to Lisp Toplevel.
>> Debug> :continue 1
>> Use a new ice cream.
>> Enter a new ice cream: 'chocolate
=> CHOCOLATE, CARAMEL, CHERRY
부수 효과 (Side Effects)
없음.
특이 상황 (Exceptional Situations)
없음.
참고 (Notes)
(restart-case expression
(name1 arglist1 ...options1... . body1)
(name2 arglist2 ...options2... . body2))
는 기본적으로 다음과 동등해요:
(block #1=#:g0001
(let ((#2=#:g0002 nil))
(tagbody
(restart-bind ((name1 #'(lambda (&rest temp)
(setq #2# temp)
(go #3=#:g0003))
...slightly-transformed-options1...)
(name2 #'(lambda (&rest temp)
(setq #2# temp)
(go #4=#:g0004))
...slightly-transformed-options2...))
(return-from #1# expression))
#3# (return-from #1#
(apply #'(lambda arglist1 . body1) #2#))
#4# (return-from #1#
(apply #'(lambda arglist2 . body2) #2#)))))
익명 리스타트는 일반적으로 대화형으로만 유용하고, 설명 없는 대화형 옵션은 가치가 거의 없어요. 구현들은 컴파일 타임에 익명 리스타트가 보고 정보 없이 쓰이면 경고를 주도록 권장돼요. 런타임엔 이 오류가 디버거 진입 시 발견될 수 있는데, 오류를 신호하면 디버거로 재귀 진입해 또 다른 재귀 오류를 일으킬 수 있으므로, 디버거는 이런 문제가 생기면 오류를 신호하지 말고 어떤 표시만 하도록 권장돼요.
(restart-case (signal fred)
(a ...)
(b ...))
==
(restart-case
(with-condition-restarts fred
(list (find-restart 'a)
(find-restart 'b))
(signal fred))
(a ...)
(b ...))
더 알아보기
restart-bind— 리스타트를 직접 설치하는 저수준 매크로with-simple-restart— 간단한 리스타트 설치 매크로