Common lisp restart to condition binding

2019-05-29 19:28发布

问题:

I'm learning common lisp in my free time and have a questions about the condition system.

When we handle an error in common lisp we specify error-type in a handler to determine which error to handle. Between raising and handling an error I can place some restarts (for example with restart-case) but I cannot specify in restart an error type.

For example, assume I have a function that takes a string and a stream, sends string to stream and read the response from stream and returns it. Assume that if my message is wrong I read from stream an error response. And I want to raise an error and bind a restart that asks for new message like this:

(defun process-message (stream raw-message)
  (let ((response (get-response stream raw-message)))
    (restart-case
        (when (response-error-p response)
          (error 'message-error :text response))
      (change-raw-message (msg)
        (process-message stream msg)))))

Now assume that the message is complicated and I got another function send-command at higher level that can create a message from some arguments and calls the process-message. I want to bind another restart recreate-command-message that will allow user to send new command from arguments if 'message-error acquires. This restart could be places in restart-case at process-message, but it is not fully correct because process-message should not know about such high-level function like send-command and the return values can differ.

But now the stream errors (such as EOF etc.) will be thrown throw recreate-command-message and if socket will fail the recreate-command-message restart will be available in some super-high-level socket-error handler and this restart will be useless and idiomatically wrong.

Is this a program design problem and a program should be designed to avoid such problems, or I just cannot find how to bind restart to error type or I do not understand the condition system correctly?

Thanks.

回答1:

Maybe this helps:

(define-condition low-level-error (simple-error)
  ()
  (:report (lambda (c s)
             (format s "low level error."))))

(define-condition high-level-error (simple-error)
  ()
  (:report (lambda (c s)
             (format s "high level error."))))

(defun low-level (errorp)
  (restart-case
      (when errorp (error 'low-level-error))
    (go-on ()
      :report "go on from low-level"
      t)))

(defun high-level (high-level-error-p low-level-error-p)
  (restart-case
      (progn
        (when high-level-error-p (error 'high-level-error))
        (low-level low-level-error-p))
    (go-on ()
      :report "go on from high level"
      :test (lambda (c) (typep c 'high-level-error))
      t)))

Try invoking high-level with different values (t or nil) for its arguments and check in the debugger if the respective available restarts fit your needs. The high level restart will only be seen if a high level error is signalled, and since the restart for the higher level is kept up the stack, the lower level function won't have to know about high level means to recover.

For your particular use-case, if I understand you correctly, this would mean: Establish your recreate-command-message restart to re-invoke process-message in send-command, and make it only available for high level errors.

As you probably know after reading the PCL chapter Vsevolod linked above, actually handling those errors, i.e. deciding which restarts to invoke, is done with handler-bind and handler-case.