RESTART-CASE — 리스타트를 선언적으로 설치하는 매크로

RESTART-CASE — 리스타트를 선언적으로 설치하는 매크로

restart-bind를 좀 더 쓰기 편하게 다듬은 상위 매크로예요. 리스타트할 지점을 이름과 lambda list, 옵션으로 선언하면, 몸체에서 조건으로 제어를 넘겨받는 구조를 쉽게 만들 수 있어요.

출처: CLHS: Macro RESTART-CASE

시그니처

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 식.
  • declarationdeclare 식; 평가되지 않아요.
  • form — 폼.
  • resultsrestartable-form의 평가로 얻은 값, 또는 선택된 절에서 실행된 마지막 form이 돌려준 값, 또는 nil.

설명 (Description)

restart-case는 절들(clauses)이 제어가 옮겨질 특별한 지점으로 의미를 갖는 동적 환경에서 restartable-form을 평가해요. restartable-form이 끝까지 실행되고 값을 돌려주면, 그 값이 restart-case가 돌려주는 값이 되고 처리가 끝나요. restartable-form 실행 중에는 어떤 코드든 invoke-restart를 통해 절들 중 하나로 제어를 옮길 수 있어요. 이동이 일어나면 그 절 몸체의 폼들이 평가되고, 마지막 폼이 돌려준 값이 restart-case의 값이 돼요. 이 경우 절 실행 전에 restartable-form 주위에 설치된 리스타트들이 더 이상 활성화되지 않도록 동적 상태가 적절히 풀려요(unwind).

선택된 절에 form이 하나도 없으면 restart-casenil을 돌려줘요.

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-formcarsignal, 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 — 간단한 리스타트 설치 매크로