Am Fr., 30. Sept. 2022 um 23:22 Uhr schrieb John Cowan <[email protected]>:

>
>
> On Fri, Sep 30, 2022 at 4:07 PM Marc Nieper-Wißkirchen <
> [email protected]> wrote:
>
>
>>
>>> 1. I strongly recommend that a faithful implementation of CL's
>>> `unwind-protect` be added to this SRFI.  Scheme programmers have a tendency
>>> to treat `dynamic-wind` as if it served the same purpose, which it does
>>> not; making it available under its own name will encourage its proper use.
>>> The definition is available at <http://clhs.lisp.se/Body/s_unwind.htm>.
>>>
>>
>> I have never taken a look at CL's `unwind-protect`.  You seem to imply
>> that it is different from a `dynamic-wind` without a BEFORE thunk.  Can you
>> explain the purpose/mechanics of `unwind-protect`?  I can then add it.u
>>
>
> See Kent Pitman's note at <
> http://www.nhplace.com/kent/PFAQ/unwind-protect-vs-continuations-original.html>
> and Dorai Sitaram's paper at <
> https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.79.3107&rep=rep1&type=pdf>.
> The general idea is that any exit from the main thunk causes the
> after-thunk to be executed once and only once.  The paradigm case is
> opening a file in the main thunk and closing it in the after-thunk.
>

I don't fully understand why Kent Pitman cites `call-with-input-file` as
Scheme's semantics for it is different from the semantics he wants for
`unwind-protect` in the presence of first-class continuations.

As far as I understand it, `call-with-input-file` in Scheme can be defined
along the following lines:

(define call-with-input-file
  (lambda (filename proc)
    (define port ...)
    (define closed? (make-atomic-flag))
    (let-values ((vals (proc port)))
      (unless (atomic-flag-test-and-set! closed?)
        (close-port port))
      (apply values vals))))

In particular, nothing is guaranteed when control enters the procedure even
after `call-with-input-file` has returned.  Or if the procedure does not
return at all.  (See SRFI 230 for the atomic flag, which is necessary for a
multi-threaded context.)

With this semantics, a definition of `unwind-protect` could be

(define-syntax unwind-protect
  (syntax-rules ()
    ((unwind-protect protected-form cleanup-form)
     (let ((unwound? (make-atomic-flag)))
       (let-values ((vals protected-form))
         (unless (atomic-flag-test-set! unwound?)
           cleanup-form)
         (apply values vals))))))

But while this mirrors the semantics of `call-with-input-file`, it is not
what `unwind-protect` is supposed to do.  Non-local exits, e.g. due to
`guard` exception handlers, would not be protected.

So we have to guard against non-local exits as well.  Kent Pitman describes
the problem that due to the presence of `call/cc` we don't know whether the
control will re-enter the protect form.  The simplest way to cope with it
is to forbid re-entering.  This can be done with the SRFI 226 primitives,
and we are led to the following definition:

(define-syntax unwind-protect
  (syntax-rules ()
    ((unwind-protect protected-form cleanup-form)
     (call-with-continuation-barrier
       (lambda ()
         (dynamic-wind
           (lambda () (values))
           (lambda () protected-form)
           (lambda () cleanup-form)))))))

Is this close to what you have in mind?


>
>
>> Instead of returning a thunk that raises an error, we could also specify
>> that #f is delivered as a "thunk" instead.  Calling #f would also raise an
>> error, but it would also enable to use this particular iterator protocol
>> for lists that contain the value #f (not relevant here, but possibly
>> somewhere else).  What do you think?
>>
>
> It's a less specific error: "#f is not a procedure" is mysterious compared
> to "attempt to iterate the empty list".  I recommend that a general version
> of iterators return a Maybe as well as the next iterator.
>

Okay, I will then make it return a procedure that raises an assertion
violation when called.

If I were to design my own iterator protocol, I would make the iterator
procedure take two continuation procedures, a success procedure that would
be invoked on the iterator's tail and the next value, and a failure thunk.
This is the most direct translation from the abstract iterator semantics.
All other protocols can be simply derived:

(define iterator->generator
  (lambda (it)
    (lambda ()
      (it (lambda (new-it val) (set! it new-it) val)
          (lambda () (eof-object))))))

(define iterator->maybe-iterator
  (lambda (it)
    (let f ((it it))
      (lambda ()
        (it (lambda (new-it val)
              (values (just val) (f new-it)))
            (lambda () (values (nothing val) (lambda ()
(assertion-violation #f "attempt to iterate the empty list")))))))))

[...]


>>
>> 4.  I don't think that &thread-timeout is necessarily an &error or even
>>> &serious; for example, you may be running a background thread to do work
>>> for a while, but want to cut it off if it runs too long.  It should be a
>>> direct subtype of &condition.
>>>
>>
>> I have to think about it.  Wouldn't your reasoning apply to
>> `&uncaught-exception` and `&thread-already-terminated` as well?  From the
>> definition of the conditions in R6RS, `&serious` seems to be a better fit
>> than `&error`.
>>
>> Looking at it from a different perspective: What could go wrong if we
>> left it as is?
>>
>
> The CL definition of `serious-condition` (CL is the source of much of the
> R6RS hierarchy) says: "All conditions serious enough to require interactive
> intervention if not handled should inherit from the type
> serious-condition."  But whether this is true of any of these three
> conditions (I only noticed &thread-timeout, but you are right that the same
> logic applies to all) is implementation-dependent.
>
> Whether this makes a difference depends on the behavior of the primordial
> exception handler.  If there is a debugger, the primordial handler should
> always invoke it if the condition is serious; a non-serious condition,
> however, might simply be logged and the program continued.
>

The primordial handler will return on a non-serious condition with
unspecified values.  This will result in a serious condition if the
non-serious condition has been raised non-continuably.  It can make sense
to specify that the thread-specific condition we talk about here (like the
timeout condition) be raised continuably because this allows a handler to
substitute values (e.g. by retrying with a different thread that may not
time out).  But if the condition is non-serious at the same time, timeout
errors can easily go unnoticed.

So it looks as if the following is a good idea:

Make `&thread` inherit from `&serious`, not `&error`, and raise some
conditions continuably.

What do you think?



>
> Editorial note: &uncaught-exception-error -> &uncaught-exception.  There
> may be other mistakes of this type.
>

Thanks for this catch!



>
>
>> A thread runner is a procedure that takes one argument, a thread.  When a
>> thread runner is called with such an argument, the thread is registered
>> with the thread runner.
>>
>
> It is not clear what should happen if a thread is registered with more
> than one thread-runner.  Tying the thread's creation to a specific
> thread-runner avoids that problem, although if the thread were marked as
> being registered or unregistered that would work as well.
>

I don't see this as a problem.  More than one thread-runner would wait for
the same thread to terminate, which would pose no problem.



> (This works well because the creation and the start of threads are
>> separated.)  When the thread runner is called with no argument, we could
>> make it unregister and return an arbitrary thread it holds.
>>
>
> This defeats the scoping for which thread-runners were devised.  However,
> the concerns could be separated by providing (register-thread thread-runner
> thread), which fails if the thread is already registered, as well as
> (with-thread-runner proc).
>

I don't see a problem here, but maybe I don't understand.



>
>
>> The dynamic environment holds the current exception handler (according to
>> the Scheme reports); it is not part of the parameterization, though.
>>
>
> Fair enough: in this case, that should be noted in the SRFI.
>

Noted.



> I don't think this incompatibility is a big problem as we already have the
>> incompatibility in the dynamic environment, in which a promise is forced.
>> I propose something like `(scheme promise)` and deprecating the old
>> `(scheme lazy)`.
>>
>
> Concedo.
>

Reply via email to