From: Edi Weitz
Subject: Two questions about restarts
Date: 
Message-ID: <87wuekf4n4.fsf@bird.agharta.de>
Hi!

The part of my application my question is about can be boiled down to
this code:

  (define-condition foo-error (simple-error)
    ())

  (defun do-something-with (x)
    (car x))

  (defun foo (user-fn arg)
    (do-something-with
        (restart-case
            (or (funcall user-fn arg)
                (error 'foo-error
                       :format-control "Value associated with ~S is NIL"
                       :format-arguments (list arg)))
          (other-value (other-value)
            :report (lambda (stream)
                      (format stream "Use another value for ~S: " arg))
            :interactive (lambda ()
                           (format t "Enter another value for ~S: " arg)
                           (multiple-value-list (eval (read))))
            :test (lambda (condition)
                    (typep condition '(or null foo-error)))
            other-value))))

The idea is that FOO will be fed with a user-supplied function USER-FN
and argument ARG. USER-FN is responsible for generating a value out of
ARG which in turn will be fed into DO-SOMETHING-WITH. I want to make
sure that USER-FN doesn't return NIL and provide a restart for this
situation. I also want this restart to only appear if this particular
situation occurs.

Now, this seems to work as intended (empty lines deleted for brevity):

Case 1. User supplies reasonable function

  * (foo #'cdr '(1 2 3))
  2

Case 2. User function returns NIL, restart is active and can be used.

  * (foo (constantly nil) '(1 2 3))
  Error in function FOO:  Value associated with (1 2 3) is NIL
  Restarts:
    0: [OTHER-VALUE] Use another value for (1 2 3):
    1: [ABORT      ] Return to Top-Level.
  Debug  (type H for help)
  (FOO #<Closure Over Function "DEFUN CONSTANTLY" {485C4A71}> (1 2 3))
  Source: (ERROR 'FOO-ERROR
                 :FORMAT-CONTROL
                 "Value associated with ~S is NIL"
                 :FORMAT-ARGUMENTS
                 ...)
  0] :c 0
  :C
  0] Enter another value for (1 2 3): '(2 3)
  2

Case 3. An unrelated error occurs - restart doesn't show up.

  * (foo (lambda (x) (/ 1 x)) 0)
  Arithmetic error DIVISION-BY-ZERO signalled.
  Operation was KERNEL::DIVISION, operands (1 0).
  Restarts:
    0: [ABORT] Return to Top-Level.
  Debug  (type H for help)
  (KERNEL::INTEGER-/-INTEGER 1 0)
  Source: Error finding source:
  Error in function DEBUG::GET-FILE-TOP-LEVEL-FORM:  Source file no longer exists:
    target:code/numbers.lisp.

Now, here are my questions:

A. My OTHER-VALUE seems like re-inventing the wheel to me because CL
   already has a USE-VALUE restart. However, I couldn't figure out
   from staring at the spec how to provide USE-VALUE instead of my
   home-grown restart. Any pointers?

B. My initial assumption was that the :test function should have been

     (lambda (condition)
       (typep condition 'foo-error))

   but this doesn't work:

      * (foo (constantly nil) '(1 2 3))
      Error in function FOO:  Value associated with 1 is NIL
      Restarts:
        0: [OTHER-VALUE] Use another value for (1 2 3):
        1: [ABORT      ] Return to Top-Level.
      Debug  (type H for help)
      (FOO #<Closure Over Function "DEFUN CONSTANTLY" {484D7431}> (1 2 3))
      Source: (ERROR 'FOO-ERROR
                     :FORMAT-CONTROL
                     "Value associated with ~S is NIL"
                     :FORMAT-ARGUMENTS
                     ...)
      0] :c 0
      :C
      0] Restart #<RESTART {484D75F5}> is not active.
      Error flushed ...

   9.1.4.2.3 says that the one argument to the restart's test is a
   condition or NIL but I couldn't find out when exactly this argument
   would be NIL and what that means.

   I thought about using WITH-CONDITION-RESTARTS instead but there it
   says "Usually this macro is not used explicitly in code, since
   restart-case handles most of the common cases in a way that is
   syntactically more concise."

Thanks in advance for your help,
Edi.

From: Kent M Pitman
Subject: Re: Two questions about restarts
Date: 
Message-ID: <sfwwuejojpt.fsf@shell01.TheWorld.com>
Edi Weitz <···@agharta.de> writes:

> Hi!
> 
> The part of my application my question is about can be boiled down to
> this code:
> 
>   (define-condition foo-error (simple-error)
>     ())
> 
>   (defun do-something-with (x)
>     (car x))
> 
>   (defun foo (user-fn arg)
>     (do-something-with
>         (restart-case
>             (or (funcall user-fn arg)
>                 (error 'foo-error
>                        :format-control "Value associated with ~S is NIL"
>                        :format-arguments (list arg)))
>           (other-value (other-value)
>             :report (lambda (stream)
>                       (format stream "Use another value for ~S: " arg))
>             :interactive (lambda ()
>                            (format t "Enter another value for ~S: " arg)
>                            (multiple-value-list (eval (read))))
>             :test (lambda (condition)
>                     (typep condition '(or null foo-error)))
>             other-value))))
> 
> The idea is that FOO will be fed with a user-supplied function USER-FN
> and argument ARG. USER-FN is responsible for generating a value out of
> ARG which in turn will be fed into DO-SOMETHING-WITH. I want to make
> sure that USER-FN doesn't return NIL and provide a restart for this
> situation. I also want this restart to only appear if this particular
> situation occurs.
> 
> Now, this seems to work as intended (empty lines deleted for brevity):
> 
> Case 1. User supplies reasonable function
> 
>   * (foo #'cdr '(1 2 3))
>   2
> 
> Case 2. User function returns NIL, restart is active and can be used.
> 
>   * (foo (constantly nil) '(1 2 3))
>   Error in function FOO:  Value associated with (1 2 3) is NIL
>   Restarts:
>     0: [OTHER-VALUE] Use another value for (1 2 3):
>     1: [ABORT      ] Return to Top-Level.
>   Debug  (type H for help)
>   (FOO #<Closure Over Function "DEFUN CONSTANTLY" {485C4A71}> (1 2 3))
>   Source: (ERROR 'FOO-ERROR
>                  :FORMAT-CONTROL
>                  "Value associated with ~S is NIL"
>                  :FORMAT-ARGUMENTS
>                  ...)
>   0] :c 0
>   :C
>   0] Enter another value for (1 2 3): '(2 3)
>   2
> 
> Case 3. An unrelated error occurs - restart doesn't show up.
> 
>   * (foo (lambda (x) (/ 1 x)) 0)
>   Arithmetic error DIVISION-BY-ZERO signalled.
>   Operation was KERNEL::DIVISION, operands (1 0).
>   Restarts:
>     0: [ABORT] Return to Top-Level.
>   Debug  (type H for help)
>   (KERNEL::INTEGER-/-INTEGER 1 0)
>   Source: Error finding source:
>   Error in function DEBUG::GET-FILE-TOP-LEVEL-FORM:  Source file no longer exists:
>     target:code/numbers.lisp.
> 
> Now, here are my questions:
> 
> A. My OTHER-VALUE seems like re-inventing the wheel to me because CL
>    already has a USE-VALUE restart. However, I couldn't figure out
>    from staring at the spec how to provide USE-VALUE instead of my
>    home-grown restart. Any pointers?

s/other-value/use-value/
 
> B. My initial assumption was that the :test function should have been
> 
>      (lambda (condition)
>        (typep condition 'foo-error))
> 
>    but this doesn't work:
> 
>       * (foo (constantly nil) '(1 2 3))
>       Error in function FOO:  Value associated with 1 is NIL
>       Restarts:
>         0: [OTHER-VALUE] Use another value for (1 2 3):
>         1: [ABORT      ] Return to Top-Level.
>       Debug  (type H for help)
>       (FOO #<Closure Over Function "DEFUN CONSTANTLY" {484D7431}> (1 2 3))
>       Source: (ERROR 'FOO-ERROR
>                      :FORMAT-CONTROL
>                      "Value associated with ~S is NIL"
>                      :FORMAT-ARGUMENTS
>                      ...)
>       0] :c 0
>       :C
>       0] Restart #<RESTART {484D75F5}> is not active.
>       Error flushed ...
> 
>    9.1.4.2.3 says that the one argument to the restart's test is a
>    condition or NIL but I couldn't find out when exactly this argument
>    would be NIL and what that means.

When you do FIND-RESTART or COMPUTE-RESTARTS without a condition
argument (which you mostly shouldn't ever do, though there are some
times you might have no choice, especially in debugging).
 
>    I thought about using WITH-CONDITION-RESTARTS instead but there it
>    says "Usually this macro is not used explicitly in code, since
>    restart-case handles most of the common cases in a way that is
>    syntactically more concise."

This last paragraph shows the problem.

You don't want a test function at all.

The "common cases" alluded to include things like in the following code
example:

  (or (funcall user-fn arg)
      (restart-case (error 'foo-error
                           :format-control "Value associated with ~S is NIL"
                           :format-arguments (list arg))
        ...))

When you do this, (ERROR ...) is specially recognized and the restart is
automatically "associated with" the condition, so that FIND-RESTART only
ever considers the restart for your specific error or for situations where
no error condition is used.

RESTART-CASE does not specially recognize an (OR ...) so does not associate
the condition with a restart.


The kludge you have done patches most of your failure to correctly associate
the condition with a restart, however it still leaves open the possibility
that this restart will be visible recursively inside a case where the funcall
gets an error and that error is either a FOO-ERROR or something that 
recursively results in a FOO-ERROR, neither of which are situations you mean
to address.
From: Edi Weitz
Subject: Re: Two questions about restarts
Date: 
Message-ID: <87znjf91ky.fsf@bird.agharta.de>
Kent M Pitman <······@world.std.com> writes:

> > A. My OTHER-VALUE seems like re-inventing the wheel to me because
> >    CL already has a USE-VALUE restart. However, I couldn't figure
> >    out from staring at the spec how to provide USE-VALUE instead
> >    of my home-grown restart. Any pointers?
> 
> s/other-value/use-value/

Thanks. I had the same discussion with James Anderson by private
email. Looks like I had the strange misconception that somewhere out
there there already is a USE-VALUE restart I could deploy. Turns out
there's just the name and the "convenience function" USE-VALUE.

> >    9.1.4.2.3 says that the one argument to the restart's test is a
> >    condition or NIL but I couldn't find out when exactly this argument
> >    would be NIL and what that means.
> 
> When you do FIND-RESTART or COMPUTE-RESTARTS without a condition
> argument (which you mostly shouldn't ever do, though there are some
> times you might have no choice, especially in debugging).
>  
> >    I thought about using WITH-CONDITION-RESTARTS instead but there it
> >    says "Usually this macro is not used explicitly in code, since
> >    restart-case handles most of the common cases in a way that is
> >    syntactically more concise."
> 
> This last paragraph shows the problem.
> 
> You don't want a test function at all.
> 
> The "common cases" alluded to include things like in the following code
> example:
> 
>   (or (funcall user-fn arg)
>       (restart-case (error 'foo-error
>                            :format-control "Value associated with ~S is NIL"
>                            :format-arguments (list arg))
>         ...))
> 
> When you do this, (ERROR ...) is specially recognized and the
> restart is automatically "associated with" the condition, so that
> FIND-RESTART only ever considers the restart for your specific error
> or for situations where no error condition is used.
> 
> RESTART-CASE does not specially recognize an (OR ...) so does not
> associate the condition with a restart.

Oh, I see. I had read this but managed to forget about it because I
originally didn't use the ERROR function but a wrapper I had written
around it.

So, what's the purpose of the :TEST slot of RESTART-CASE if it isn't
used to associate restarts with errors?

Thanks,
Edi.
From: Peter Seibel
Subject: Re: Two questions about restarts
Date: 
Message-ID: <m34r1m9syu.fsf@javamonkey.com>
Edi Weitz <···@agharta.de> writes:

> So, what's the purpose of the :TEST slot of RESTART-CASE if it isn't
> used to associate restarts with errors?

The test function associated with the restart provides a more general
way to decide whether a particular restart is actually applicable.
Kent Pitman gave some examples recently in:

  Message-ID: <···············@shell01.TheWorld.com>.

If you think of a restart as an offer to resume a computation that has
gone off the expected path (as they are when the condition signaled is
an error), it makes sense to only present restarts that actually have
some hope of being able to get the computation back on track. When
handling conditions interactively, it clearly makes for a less
cluttered user interface to only present restarts that might actually
be useful. And if you are using programatic condition handlers, the
error recovery protocol may need to base its "decision" about how to
recover on what restarts can actually help out.

For instance, to expand on the second example Kent gave in the post
referenced above, a restart that offers to clear up some disk space
("empty the recycle bin") is only really applicable if there's stuff
in the recycle bin. A condition handler might be bound that tries to
recover from certain i/o errors by invoking a empty-the-recycle-bin
restart if it's available and then retrying (via some oter relatively
local restart) the failing operation. But if there is no
empty-the-recycle-bin restart available it may fall back on a
higher-level restart (such as reporting an error to the user or
retrying the operation using a different disk.) Obviously, if the
empty-the-recycle-bin was always available, even if the recycle bin
was already empty, this condition handler would get stuck in a loop
constantly trying to empty the recycle bin and retry. (Assuming, of
course, that emptying and empty recycle bin didn't itself signal some
other error.)

The main difference, it seems, between associating restarts with
particular conditions and filtering applicable restarts via test
functions is that restarts can only (conveniently) be associated with
a condition quite near the point the condition is signalled while a
high-level restart can use a test function to decide, when the
applicable restarts list is computed, whether it actually applies.

Since the empty-the-recycle-bin restart is generally applicable to all
attempts to write to disk it makes sense to bind it at a fairly high
level rather than at all the places that might attempt operations that
could possibly fail and then be repaired by emptying the recycle bin.
So it is not tied to a particular condition--it really is a general
purpose recovery strategy. Just one that might not always be
applicable.

-Peter

-- 
Peter Seibel                                      ·····@javamonkey.com

         Lisp is the red pill. -- John Fraser, comp.lang.lisp
From: lin8080
Subject: Re: Two questions about restarts
Date: 
Message-ID: <3F302B92.79C7DB26@freenet.de>
Peter Seibel schrieb:

>       Lisp is the red pill. -- John Fraser, comp.lang.lisp

aha ...

no lisp is the red and the blue pill
because you can wake up in your bed and think you are dreaming, till you
look at your screen.

stefan
From: Peter Seibel
Subject: Re: Two questions about restarts
Date: 
Message-ID: <m3adbd9196.fsf@javamonkey.com>
Kent M Pitman <······@world.std.com> writes:

> Edi Weitz <···@agharta.de> writes:

[snip up to code in question]

> >   (define-condition foo-error (simple-error)
> >     ())
> > 
> >   (defun do-something-with (x)
> >     (car x))
> > 
> >   (defun foo (user-fn arg)
> >     (do-something-with
> >         (restart-case
> >             (or (funcall user-fn arg)
> >                 (error 'foo-error
> >                        :format-control "Value associated with ~S is NIL"
> >                        :format-arguments (list arg)))
> >           (other-value (other-value)
> >             :report (lambda (stream)
> >                       (format stream "Use another value for ~S: " arg))
> >             :interactive (lambda ()
> >                            (format t "Enter another value for ~S: " arg)
> >                            (multiple-value-list (eval (read))))
> >             :test (lambda (condition)
> >                     (typep condition '(or null foo-error)))
> >             other-value))))

[snip]

> The kludge you have done patches most of your failure to correctly
> associate the condition with a restart, however it still leaves open
> the possibility that this restart will be visible recursively inside
> a case where the funcall gets an error and that error is either a
> FOO-ERROR or something that recursively results in a FOO-ERROR,
> neither of which are situations you mean to address.

Just to make sure I have this, right, are any of the three variants
below a non-kludgy way to do what Edi wanted?
 
  (defun foo (user-fn arg)
    (do-something-with
        (restart-case
            (let* ((e (make-condition
                       'foo-error 
                       :format-control "Value associated with ~S is NIL"
                       :format-arguments (list arg))))
              (with-condition-restarts e
                (list (find-restart 'use-value nil))
                (or (funcall user-fn arg) (error e))))
          (use-value (other-value)
            :report
            (lambda (stream)
              (format stream "Use another value for ~S: " arg))
            :interactive
            (lambda ()
              (format *query-io* "Enter another value for ~S: " arg)
              (multiple-value-list (eval (read *query-io*))))
            other-value))))

  (defun foo (user-fn arg)
    (do-something-with
        (or (funcall user-fn arg) 
            (restart-case
                (error
                 'foo-error 
                 :format-control "Value associated with ~S is NIL"
                 :format-arguments (list arg))
              (use-value (other-value)
                :report
                (lambda (stream)
                  (format stream "Use another value for ~S: " arg))
                :interactive
                (lambda ()
                  (format *query-io* "Enter another value for ~S: " arg)
                  (multiple-value-list (eval (read *query-io*))))
                (assert (not (null other-value)) (other-value))
                other-value)))))

  (defun foo (user-fn arg)
    (loop for value = 
          (or (funcall user-fn arg) 
              (restart-case
                  (error
                   'foo-error 
                   :format-control "Value associated with ~S is NIL"
                   :format-arguments (list arg))
                (use-value (other-value)
                  :report
                  (lambda (stream)
                    (format stream "Use another value for ~S: " arg))
                  :interactive
                  (lambda ()
                    (format *query-io* "Enter another value for ~S: " arg)
                    (multiple-value-list (eval (read *query-io*))))
                  other-value)))
          until value
          finally (return (do-something-with value))))

-Peter

-- 
Peter Seibel                                      ·····@javamonkey.com

         Lisp is the red pill. -- John Fraser, comp.lang.lisp
From: Edi Weitz
Subject: Re: Two questions about restarts
Date: 
Message-ID: <871xwonq4v.fsf@bird.agharta.de>
Edi Weitz <···@agharta.de> writes:

>   * (foo (constantly nil) '(1 2 3))
>   Error in function FOO:  Value associated with 1 is NIL
>   Restarts:
>     0: [OTHER-VALUE] Use another value for (1 2 3):
>     1: [ABORT      ] Return to Top-Level.
>   Debug  (type H for help)
>   (FOO #<Closure Over Function "DEFUN CONSTANTLY" {484D7431}> (1 2 3))
>   Source: (ERROR 'FOO-ERROR
>                  :FORMAT-CONTROL
>                  "Value associated with ~S is NIL"
>                  :FORMAT-ARGUMENTS
>                  ...)
>   0] :c 0
>   :C
>   0] Restart #<RESTART {484D75F5}> is not active.
>   Error flushed ...

For the record: This behaviour (of CMUCL 18e) of first offering a
restart and then subsequently saying it's not active is a bug and had
already been fixed in the CVS version of CMUCL when I posted this.

Thanks to Pekka P. Pirinen who pointed this out to me and to Gerd
Moellmann who currently seems to fix most bugs before they are even
reported.

Edi.