At 09:04 PM 10/12/98 -0700, Sheldon Nicholl wrote:
I really feel like I'm going out on a limb by saying this, but I think Smalltalk would be a better language if non-local returns were removed entirely. They cause trouble all across the board, from the Debugger to the VM to beginning Smalltalkers trying to learn the language.
Interesting idea, but could you get away with it?
I think so. First, let me clarify my proposal: redefine the meaning of ^ to be a local return (thisContext sender), not a non-local return (thisContext home sender), and use continuations for non-local stuff. I consider the use of exceptions here (or similar mechanisms) a distant dark-horse alternative, to be used only if no one wants to use continuations. That is, exceptions should be used for exceptions, not to get non-local returns.
For example, would the following be illegal
stream atEnd ifTrue: [^self] ...
1 to: 3 do: [:x | (self foo: x) ifTrue: [^self] ...]
[x < 4] whileTrue: [(self foo: x) ifTrue: [^self] ...]
I'll be a bit slippery here and give four alternative proposals, ranked in increasing order of preferability:
(1) The Status-Quo Solution (2) The Language Realignment Solution (3) The Clean Programming Solution (4) The Continuation Solution
(1) The Status-Quo Solution: Since all of them use constructs like whileTrue:, ifTrue:, and so on which are always inlined, the reinterpretation of ^ as a local return will work since these constructs don't create a context. Not seriously recommended.
(2) The Language Realignment Solution: We admit that the automatic inlining of these constructs (ifTrue:, etc.) by the compiler constitues the legislation of them as built-in constructs like other languages, such as Pascal (if-then, etc.). No message is sent, no context is created, they can't be browsed on, can't be traced, the code in their methods is not executed, can't be changed, and so on. Just like language constructs.
The outcome of this is to realign the language by creating special constructs for them which are recognized by the Compiler. That is, we only change the front-end syntax; what comes out the end in bytecode will hardly change, if at all. Once they become special constructs, blocks are unnecessary, so the reinterpretation of ^ as a local return works naturally.
(3) The Clean Programming Solution: I feel like I'm being a bit unfair here, since most of the examples given above seem more like theoretical expressions than examples of actual use, but I'll take them as they stand.
The examples also allow me to add to the list of troubles caused by non-local returns: "questionable" style.
stream atEnd ifTrue: [^self]
Why not just say:
stream atEnd ifFalse: [...the rest of the method...]
or maybe better:
stream atEnd ifFalse: [self doTheRest]
I think I can answer this question: because it looks terrible. I'll admit I've used it countless times, but almost always with a twinge of conscience.
But we can't let imperfections in the syntax of the language push us into a questionable programming practice.
1 to: 3 do: [:x | (self foo: x) ifTrue: [^self] ...]
My gut reaction here would be to try using a detect: e.g.,
(1 to: 3) detect: [:i | self foo: x ...]
I can't think of a time over the last couple of years (at least) when I've had to use anything but a detect: to get the effect of jumping out of a loop prematurely. And detect: can be rewritten to avoid the non-local return. Or if you want something more general, how about something like
aCollection do: iterationBlock until: exitBlock
which does the iterationBlock until exitBlock becomes true.
[x < 4] whileTrue: [(self foo: x) ifTrue: [^self] ...]
This one is a bit bizarre, since I can't think of an example in the image where one needs to jump out of the body of a while loop. My feeling is that the receiver of whileTrue: should be powerful enough to detect any escape conditions. So why not move (self foo: x) into the receiver block?
(4) The Continuation Solution. This is the original proposal, to use continuations for non-local movements. So in those rare cases when it really is appropriate to jump further than one's sender, the machinery is there, and it's clean every time.
glenn
--Sheldon
P.S. Getting back to Tim Rowledge's original comment:
Unwind support is needed to handle the case of a non-local return merrily skipping past your unwind protection block, leaving your files open, sockets flailing or other catastrophes imminent. A non-local return can jump from one context back up to another without passing Go, thereby causing untold trouble. The VM mod would scan up the stack from source to target, checking for marked contexts on the way; any marked context found woud be sent a suitable message, somewhat like #cannotReturn:
I just want to plead that in case something like this is adopted, that it be done at the image level. It involves far too many operations, way too much intelligence for something that should be in the VM, which should consist of simple clean primitives only. Unwinding is something that a programmer may want to modify, which is another argument for putting it in the image.
squeak-dev@lists.squeakfoundation.org