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. >
