problem-solving
problem-solving copied to clipboard
`next`, `last`, `redo` shouldn't fall through subroutines
Inspired by https://github.com/Raku/doc/issues/4255 because it seems like this is an unnecessary trap to work with.
The synopses actually do mention the dynamic nature of a bare next (as opposed to a labelled next, for example) - however, this doesn't appear anywhere in the corresponding file of the specification.
In Raku, one can declare a Block that will act just like an actual inline block, including next behavior. Subs have their own return. Implimentation differences aside (between return and next or last, that is), it really seems like a trap that next and the likes might implicitly have a broader scope of action than return, hence implicitly returning from a subroutine much "harder" than the actual return would.
If somebody wants the fall-through behavior by default, chances are they don't need/want the tight return a Sub provides and they should really be thinking about using a Block. The code will also get clearer if a subroutine is really a lexical unit with the default control structures.
My proposal would be to keep control exceptions like the ones next and last create, inside Subs, hence throwing a similar exception to a legitimately missing loop to iterate.
If somebody prefers the former (perhaps unspecified?) behavior:
- they can specify a CONTROL phaser to pass it through anyway
- I don't know how you feel about pragmas but there could be one to set the behavior for one
Subor the wholeCompUnit, for example
The topic of performance also came up from @lizmat . My stance on that is (besides the general sentiment of "it's better to be right slowly than wrong quickly"): the implementation doesn't need to be fast immediately. Loads of control structure optimizations will be available with RakuAST - in fact, most of the time, we could say it at compile time if an invalid next is used in a subroutine, by traversing from the usage of next towards the root Sub. At the very least, we can omit special handling when it's clear that there are no such control statements, or they are clearly scoped in a loop inside the Sub.
The synopses actually do mention the dynamic nature of a bare
next(as opposed to a labellednext, for example) - however, this doesn't appear anywhere in the corresponding file of the specification.
See https://github.com/Raku/roast/blob/master/S04-statements/last.t#L31-L39 for last.
The synopses actually do mention the dynamic nature of a bare
next(as opposed to a labellednext, for example) - however, this doesn't appear anywhere in the corresponding file of the specification.See https://github.com/Raku/roast/blob/master/S04-statements/last.t#L31-L39 for
last.
I think this is the right place for design decisions anyway - so then the amendment woud be: introduce this in a new version, and modify the corresponding test.
Hmm, my (tentative) thought is that the current behavior is reflects the correct design. A different way to phase this issue would be "next, last, and redo should have lexical scope rather than dynamic scope". But, phrased like that, I'm slightly inclined to disagree – it's very clear that gather is supposed to have dynamic scope, and next/last/redo "feel" similar enough to gather that I'm inclined to think that keeping their behavior consistent is the better design.
On the other hand, lexical scope is more familiar to most programmers and I don't have strong feelings on the matter. So I'm certainly open to persuasion.
Just an example:
sub transform($v) {
next if $v.contains("2");
"<$v>"
}
say (^20).map(&transform);
Even the .map({...}) form is a good demonstration because the code block just cannot be in the same lexical scope where the iteration logic is located.
Even the
.map({...})form is a good demonstration because the code block just cannot be in the same lexical scope where the iteration logic is located.
Well, it can:
say (^20).map({
next if .contains("2");
"<$_>"
});
But, more generally, I agree that there are times when a dynamic scope to next is useful/clearer.
Well, it can:
You missed the point. The actual iterator logic is in map lexical scope, actually. :)
Even the .map({...}) form is a good demonstration because the code block just cannot be in the same lexical scope where the iteration logic is located.
The .map({...}) form uses a Block, not a Sub. I'm not arguing against this behavior for a Block but a Sub. And consequently, that example looks like a good illustration of what I find confusing without any benefits.
A different way to phase this issue would be "next, last, and redo should have lexical scope rather than dynamic scope"
I don't think this reflects the Block vs Sub distinction. It's super useful and also fairly reasonable that a Block is transparent to all control structures. It could even look up all variables dynamically - I can imagine why that might be a bad idea but it could also make some sense. However, to have a subroutine that can be safely used as an independent unit of execution, is a different story.
it's very clear that gather is supposed to have dynamic scope, and next/last/redo "feel" similar enough to gather that I'm inclined to think that keeping their behavior consistent is the better design.
It seems to me that both statements can be challenged. I do think that take could also be taken as a cunning implicit callback and be subject to evalutation whether it's really worth it to let it slip through subroutines. However, let me point out that take does indeed have more of a callback interface that next, last and the likes don't have, and maybe for the better:
use v6.*; # To enforce next, last with return values
gather for (1..10) { .take } # This is going to work
(1..10).map: { .next } # This won't - has to use `next $_`
(1..10).map: { .last } # This also won't - has to use `last $_`
Now, of course those control statements could be adopted to also have the OO-looking interface - which is also a modification that might interfere with existing code, and then we can still ask if it's really expressive or just confusing.
I'm not arguing against this behavior for a Block but a Sub.
IMO having this behavior differ between Blocks and Subs would be highly confusing.
@2colours:
The
.map({...})form uses aBlock, not aSub. I'm not arguing against this behavior for aBlockbut aSub. And consequently, that example looks like a good illustration of what I find confusing without any benefits.
Are you sure about no benefits? I consider the transform sub from my example to be very beneficial as means of code clarification and organization. .map(&transform) (or .map: &transform) seems to be way more self-explanatory than using a block with all the same logic transform provides. Basically, the routine should've be named transform-and-filter for the full clarity of it.
@codesections:
IMO having this behavior differ between Blocks and Subs would be highly confusing.
Not exactly. Think of return which is routine-scoped, so to say. The other point is that its semantics greatly differs from that of next and its "kins".
Just to throw in another code example that could potentially confuse me:
for @lists -> $x {
$x.map({ process($_); next if !$_ }
}
IMO having this behavior differ between Blocks and Subs would be highly confusing.
Well, what can I say, I think this is the whole point Blocks exist in the first place - to represent a piece of code with a scope that can be abstracted away while still providing inlining semantics. I can't see why this would be confusing - rather than Sub sometimes mimicking inlined behavior, by implicitly passing control flow back to the caller...
The other point is that its semantics greatly differs from that of next and its "kins".
Well, does it? Are they not all pragmatisms over structural code, sort of jumps that are at least guaranteed to be tied to certain control structures? I would say return and next are much more alike than take and next - I wonder if take is considered a control statement to begin with, it's not obvious at least. By semantics, it's much more like a callback.
Also, food for thought: linked part of synopsis also says For instance, next with a label will prefer to exit a loop lexotically - and this is present in the current workings as well. next BAR isn't going to fall through anything but a bare next will - does this inconsistency seem as good as it did back when this language was going to be the new Perl?
Also, if one does want the fallback behavior for a subroutine, it could still be made explicit, right? I have mentioned the idea of a pragma but at the end of the day, I wouldn't oppose something like next * either. The asterisk even plays nice with the symbolics of dynamic behavior.
.map(&transform)(or.map: &transform) seems to be way more self-explanatory than using a block with all the same logic transform provides
I'm afraid this is a "let's agree to disagree" situation - that seems to me like a misuse of a subroutine. To me, it breaks the expectations of a subroutine - the expectation that a successfully executed subroutine won't interfere with the code of the caller - after all, that's part of the reason why I abstracted it away with a subroutine and not a Block, right?
It can actually lead to weird thoughts: what return value does that subroutine even have? After all, next is not a callback, and indeed it terminates the control flow of the subroutine in the first place. Indeed, it behaves like an exception, without looking like one.
Just to throw in another code example that could potentially confuse me:
for @lists -> $x { $x.map({ process($_); next if !$_ } }
@patrickbkr Could you please elaborate? Because here the for loop seems superfluous. So far, I haven't seen anybody questioning that map˙implies a loop, therefore all possible uses would hook up there, if anywhere.