From: Larry Wall <[EMAIL PROTECTED]> Date: Mon, 2 Jan 2006 20:11:23 -0800
On Mon, Jan 02, 2006 at 06:55:24PM -0500, Bob Rogers wrote: : [2] About two-thirds of the way through A06 (search for "temporize : object attributes"), Larry says that this will be done via : closures. In order to support rezipping, such a closure would need : to accept a new value to store and return the old value. Or maybe : the .TEMP method could just return a location PMC? I would prefer the latter for the general case. As with any "rw" location, you're returning a proxy/location object with an appropriate lvalue interface. Any user-specified closures end up being methods of that object. Treating such a closure as the proxy object not only confuses getting with setting but also confuses both of those with the original identification of the location. I was not terribly consistent in carrying this idea though in the original Apocalypses. Thanks for making that clear. The advantage of supporting closures, of course, is that they are very general, and allow pretty arbitrary things to be rezipped (if you want to allow such extension). But I do agree that it's better to use something that describes exactly what you mean, and is (at least potentially) introspectable. By the way, it's getting to be a bit dangerous to quote the Apocalypses rather than the Synopses, since they've diverged somewhat, and the Synopses are being kept rather more up-to-date. The "post" block has turned into a "leave" block, for instance. (Pre/post blocks are reserved for Design-By-Contract now.) Sorry; I was hoping to cut down on the amount of reading I need to do. I still haven't even covered the full set of Apocalypses. I've also avoided subscribing to p6l for the same reason. But I suppose I need to reconsider both. Anyway, I think we'll need to recognize over the long haul that some forms of control flow are simply incompatible with certain kinds of state change, in a Haskellian monadic sense. I think if the first whack at rezipping can simply detect such situations and refuse to proceed, we'll at least be up to the Perl 5 level, even if we can't continue into a temporized area. After all, the entire Perl 5 model assumes that you only have to undo once. Yes; "life was much simpler back then . . ." Closures can be a pain, but on the other hand, they are also a lot of fun; personally, I consider them the natural vehicle for implementing nonlocal transfer of control. Which is why I care about getting things to rezip correctly. The good news, though, is that I haven't seen mention of anything that I wouldn't know how to implement reasonably -- with the exception of "Hypotheticality and Flight Recorders", which itself seems rather hypothetical. But even there, a lexical version might be a reasonable thing to consider: $x = 0; temp { $x = 1234; print $x; # prints 1234 } print $x; # prints 0 That pushes much of the work back on the compiler (which needs to run wires into the flight recorder), but that strikes me as reasonable; you don't need or want EVERYTHING captured by a flight recorder. But all that can wait. The point is that I think the proposed model is robust enough for what you want to do with it (caveat my limited understanding of same). It'd be nice to generalize from that, but Perl 6 is moving more toward using a thing we're calling "environmental" lexical variables to represent dynamic context anyway, which I think is more like what you'd call a deep binding. So from a Perl perspective, temporization is being de-emphasized somewhat. FWIW, the terms "deep" and "shallow" binding are old Lisp terms (in case you hadn't already guessed), and as used in the Lisp community, they refer to dynamic binding exclusively. (For this purpose, deep binding is clearly less efficient, which is why you don't hear those terms used much at all any more; shallow binding is taken for granted.) But in any case, I do not understand this bit, which is probably a reflection of the Synopsis reading I have to do. On the other hand, as you point out, you do have to be careful about unwinding exceptions. We'll need to do that as lazily as possible, since some exceptions will include an optional continuation to resume if the exception thrower wants to allow the exception to be "defatalized". That allows only one way to continue, though, and it's under exclusive control of the exception-thrower. Have you encountered the Common Lisp 'restart' concept? (The reference is [5], but there's a fair amount to it, so I won't try to produce a capsule summary.) The advantage of restarts is that they allow users (both throwers and catchers) to define named restart actions that can be invoked programmatically, or interactively via the debugger. Specifying that a particular action should happen whenever a given error is encountered then becomes a simple matter of binding a handler that invokes the right restart, which could even be done by a third party (i.e. neither the thrower nor the catcher). But only certain predefined restarts are possible, which makes the exception handling process a multiway negotiation where no party has full control. (IIRC, the original paper [6] makes this distribution of control an explicit goal of the design.) In fact, I suspect we'll end up handling all warnings as exceptions that are defatalized by default by an outermost warning exception handler, and it'd be a shame if, when we resume after a warning, the temporizations have all been undone. Even if they get rezipped at resumption, that's gotta be a big performance hit just to emit a warning. But that argues that warnings should be handled close to the point that the warning is emitted, at least in the case where all that is required is to suppress them. I can't resist quoting the entire WARN definition from the CMUCL implementation, which also serves as a good example of using restarts: (defun warn (datum &rest arguments) "Warns about a situation by signalling a condition formed by datum and arguments. While the condition is being signaled, a muffle-warning restart exists that causes WARN to immediately return nil." (kernel:infinite-error-protect (let ((condition (coerce-to-condition datum arguments 'simple-warning 'warn))) (check-type condition warning "a warning condition") (restart-case (signal condition) (muffle-warning () :report "Skip warning." (return-from warn nil))) (format *error-output* "~&~@<Warning: ~3i~:_~A~:>~%" condition))) nil) The SIGNAL call is what gives handlers a chance to run. In the normal case, nobody handles it, and control drops through the RESTART-CASE to the FORMAT, where the warning is printed. (SIMPLE-WARNING is not fatal because it is not a subclass of ERROR, so the default debugger handler also gives it a pass.) On the other hand, if somebody binds a condition handler to SIMPLE-WARNING that does "(invoke-restart 'muffle-warning)", then the restart does a RETURN-FROM, and the output is skipped. Since the handler is a closure, the code can examine the contents of the condition object before deciding to muffle it. And all of this happens in the dynamic context of the WARN function (inside kernel:infinite-error-protect, which sounds like a really good thing). So maybe there's some way of running CATCH blocks in the dynamic context of the thrower without rezipping twice. Have to think about the semantic ramifications of that though... Larry I'm not sure that the cost would be that large; we're only talking about tens of state-capture PMCs here. (I certainly hope.) But in any case, rezipping may not be required. At the risk of trying your patience, I will lay one more bit of Common Lisp error-handling semantics on you, as I hope it will serve as food for thought. The CL error signalling process [7] has two phases: The first phase decides which handler takes the error, and the second actually does the job. This is supported by two syntactic abstractions: HANDLER-CASE works like try/catch, and is typically implemented in terms of HANDLER-BIND, which provides the lower-level but more general mechanism. Sweeping much under the rug here, the low-level handlers are closures that are run in the dynamic environment where the error is signalled; if they decide to handle an error, they do so via a nonlocal transfer of control back to the right handler in the HANDLER-CASE environment. The upshot is that the bound handlers can be made pretty light-weight and therefore insensitive to the dynamic environment in which they run (unless, of course, they need to be), without causing the author of the HANDLER-CASE code to lose control of the environment in which the error cleanup forms run. So if you would consent to putting the "when" guard tests of each "catch" in closures, with the caveat to the user that they could be evaluated an arbitrary number of times in somebody else's backyard, Parrot could always find exactly the place to which it's going to unzip before it starts. And this leads to the biggest win of the "know before you throw" approach: You find out about unhandled errors immediately, before you've destroyed the most interesting part of the stack. That is the chief drawback of the current Parrot model; I've been ruminating about it for a while now, but have no concrete proposals yet. I'll bet that wrestling with dynamic binding will lead me to generate one (or two) -- if you or someone else doesn't beat me to it. Thanks for speaking up, and thanks especially for listening, -- Bob [5] http://www.lispworks.com/documentation/HyperSpec/Body/09_adb.htm [6] http://www.nhplace.com/kent/Papers/Exceptional-Situations-1990.html [7] http://www.lispworks.com/documentation/HyperSpec/Body/09_ad.htm