Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

break and continue out of multiple loops #5334

Open
kryptine opened this issue Jan 9, 2014 · 83 comments
Open

break and continue out of multiple loops #5334

kryptine opened this issue Jan 9, 2014 · 83 comments
Labels
breaking This change will break code speculative Whether the change will be implemented is speculative

Comments

@kryptine
Copy link

kryptine commented Jan 9, 2014

In Julia, the statements break and continue affects the nearest enclosing loop, and there is currently no way to point them to outer loops.

@JeffBezanson
Copy link
Member

I know this seems limiting, but it is quite standard. At that point, you'd almost want to add goto (#101). Do you have an interesting use case for this?

@kryptine
Copy link
Author

kryptine commented Jan 9, 2014

@JeffBezanson No, but Dart has this.

@JeffBezanson
Copy link
Member

The Dart spec is not too encouraging on this:

Labels should be avoided by programmers at all costs. The motivation for including labels in the language is primarily making Dart a better target for code generation.

@kryptine
Copy link
Author

kryptine commented Jan 9, 2014

Well, Julia is significantly faster than most dynamic languages, it is beneficial to make it a compiler target.

@carlobaldassi
Copy link
Member

I sometimes wished we had this functionality when writing multi-dimensional loops, e.g.:

for i=1:n, j=1:m
   condition() && break 2
   dosomething()
end

instead of

skip = false
for i=1:n
    for j=1:m
        condition() && (skip=true; break)
        dosomething()
    end
    skip && break
end

@StefanKarpinski
Copy link
Member

I've sometimes wanted multilevel break and continue but C-style goto would be even better.

Well, Julia is significantly faster than most dynamic languages, it is beneficial to make it a compiler target.

This is a strange idea. If you want a great compiler target, compile to LLVM.

@kryptine
Copy link
Author

kryptine commented Jan 9, 2014

@StefanKarpinski Translating to Julia would be easier (and the code would be more readable) than compiling to LLVM

@malmaud
Copy link
Contributor

malmaud commented Jan 10, 2014

@carlobaldassi I wonder if the better solution for that case is to consider nested loops defined in that syntax as one loop for the purpose of break, as in #5154.

@carlobaldassi
Copy link
Member

Yes, that is probably a more intuitive solution and I'd welcome that change. I also agree that goto is better than multilevel break/continue, if feasible.

@timholy
Copy link
Member

timholy commented Jan 10, 2014

One might get complaints from people who'd want to make a seemingly-small change like

for j = 1:n
    offset = (j-1)*size(A, 1)
    for i = 1:m
        condition() && multi-loop-break
        dosomething()

But the skip thing does work, so it's not like there's no viable way forward.

@timholy
Copy link
Member

timholy commented Jan 10, 2014

To clarify, what I meant was that if someone starting writing some code with the one-loop syntax, counting on how break works there, then made a change to the two-loop syntax so they could add one small thing, they might be dismayed to discovered that the break now works differently and might introduce a hard-to-find bug.

@StefanKarpinski
Copy link
Member

Honestly, I think the way break and continue work in multiple for loop syntax currently is so unituitive that I doubt anyone is using it. I have to desugar the syntax in my head to remember how it should work. I strongly feel that we should change the multiple iteration syntax to mean a different thing that just nested loops.

@JeffBezanson
Copy link
Member

I'm on board with that. break is a bit unfortunate in general, since it really turns a loop into a different kind of thing, no longer parallel.

@timholy
Copy link
Member

timholy commented Jan 10, 2014

@StefanKarpinski, it's not that I'm worried about people using break in multiple loops now, I'm worried about the implications if we use the same word to mean two different things, depending on whether it's written in one-loop syntax or two-loop syntax. It's something like 8 editor keystrokes to convert between

for j = 1:m, i = 1:n
    # some _huge_ function body that had a break buried somewhere deeply inside it
end

and

for j = 1:m; for i = 1:n
    # some _huge_ function body that had a break buried somewhere deeply inside it
end; end

I've made such conversions myself many times, and currently there is no penalty for doing so. Nothing else in julia changes its meaning when you do so, but if we introduce the asymmetry in break, that won't be true anymore. Since for most functions it won't actually generate an error---just different algorithmic behavior---the resulting bug might be quite hard to track down.

If we just use a different word from break, there would be no issue.

@JeffBezanson
Copy link
Member

That is why I originally made the multi-loop syntax just a syntax rewrite. With the change though, we'd be thinking about these not as nested loops but a single loop over a multi-dimensional space, like map(f, A) where A is >1d. And if we introduce for i in X..., there will be no simple way to rewrite it that exposes all of the loops. With that syntax I think it's even clearer that break should break out of the whole thing.

@timholy
Copy link
Member

timholy commented Jan 10, 2014

Don't get me wrong, I also think breaking out of the whole thing is the more intuitive behavior, even if it doesn't work that way in C, and for i in X... was indeed on my mind as well (though I noticed that most of the concrete examples I had thought of using it for were similar to maximum_rgn, which would be better expressed as a while so you can preserve iterator state for phase 2 of the algorithm).

I'm just worried about how we deprecate break in the case of ordinary single loops. Using a different word has its annoyances, but might be the safest.

@jiahao
Copy link
Member

jiahao commented Jan 10, 2014

Using a different word has its annoyances, but might be the safest.

breakall?

@StefanKarpinski
Copy link
Member

To break out of all loops in the current scope?

@jiahao
Copy link
Member

jiahao commented Jan 10, 2014

for the particular case of nested loops chained together with commas, at least.

@StefanKarpinski
Copy link
Member

Oo, that seems more confusing to me since I would expect breakall to break out of all loops I'm inside of.

@timholy
Copy link
Member

timholy commented Jan 10, 2014

Hmm, more boring than my idea of awesomebreak but probably better 😄

@StefanKarpinski
Copy link
Member

breakexactlyasmanyloopsasiwant

@timholy
Copy link
Member

timholy commented Jan 10, 2014

I like it! Can we have more such keywords please?

@StefanKarpinski
Copy link
Member

Or, in German: donaudampfschiffahrtsgesellschaftskapitän.

@malmaud
Copy link
Contributor

malmaud commented Jan 10, 2014

exit() will break out of all loops

@timholy
Copy link
Member

timholy commented Jan 10, 2014

Do you sing Blue Danube, too?

@StefanKarpinski
Copy link
Member

I do not, but I sing some mean Prince at karaoke. @JeffBezanson can bear witness.

@jiahao
Copy link
Member

jiahao commented Jan 10, 2014

breakallfurrealz()=@error("All the loops need to exit now. Please hold down the power button for at least four seconds.")

easy peasy

@timholy
Copy link
Member

timholy commented Jan 10, 2014

The power button is even more emphatic than exit() as a way of breaking out of loops. That would do it for sure!

More seriously, nbreak? It conveys the notion that it's equivalent to a countable number of breaks, even if you don't have to explicitly specify n.

Presumably we'd be planning to make it an error to use break inside a multiloop, and an error to use whateverwecallit inside a single loop?

@tknopp
Copy link
Contributor

tknopp commented Jan 10, 2014

Stefans suggestion makes me feel so privileged having umlauts on my keyboard.
"Can someone ask the german guy to write this code? We have to break out of a multiloop!" ;-)

More seriously, I would make the behavior change now and use break to break out of all loops. A break that does not break out of the complete loop feels wrong to me. If someone wants to break only out of one loop, he has to use two loops. Makes all this more orthogonal

@StefanKarpinski
Copy link
Member

I kind of like the comma separating these. Once you get what it means, it's pretty clear and general.

@StefanKarpinski
Copy link
Member

I'm reopening because I think the break, continue idea is worthwhile. Syntax: zero or more break keywords separated by commas, followed by an optional continue; having a continue before a break makes no sense and should be a syntax error.

@pao pao reopened this Mar 3, 2016
@StefanKarpinski
Copy link
Member

Right. Forgot to actually reopen. Thanks, @pao.

@GunnarFarneback
Copy link
Contributor

Implementing this in the parser is beyond me but as it happens "break; continue" is currently valid Julia syntax. The semantics are less useful but nothing a little macro magic can't fix. Proof of concept implementation as a macro in this gist for those who want to try it out.

@Keno Keno added this to the 0.6.0 milestone Oct 1, 2016
@JeffBezanson JeffBezanson modified the milestones: 1.0, 0.6.0 Oct 17, 2016
@ihnorton
Copy link
Member

x-ref a julia-users discussion: https://groups.google.com/d/msg/julia-users/byhFGOJz7tQ/xu16ktAVAAAJ

I think explicit or implicit named loops would be safer than the break, continue proposal above based on depth count. (implicit labels could be generated from the first loop-var or iterator-var name). Also, this could be easily provided outside the core language by macro sugar over @goto.

@JeffBezanson JeffBezanson modified the milestones: 2.0+, 1.0 May 2, 2017
@StefanKarpinski StefanKarpinski modified the milestones: 2.0, 1.x Jan 22, 2019
@c42f
Copy link
Member

c42f commented Aug 15, 2019

I agree with @ihnorton that explicit labels are attractive. We can reuse @label for this. To take the example from https://discourse.julialang.org/t/named-for-loops/27564 we currently have:

for i=1:3
    for j=1:3
        for k=1:3
            @info "indices" (i, j, k)
            if k == 2
                @goto end_j
            end
        end
    end
    @label end_j
end  

To extend @label, we can treat it as a verb which just creates labels end_j and continue_j from the loop counter:

for i=1:3
    @label for j=1:3
        for k=1:3
            @info "indices" (i, j, k)
            if k == 2
                @goto end_j
            end
        end
    end
end  

We'd also need @label lname while ... for while loops and for for loops with complex assignments.

This would need changes to lowering to support correct placement of continue_j and to support breaking out of try ... finally with a @goto (ensuring the finally block is executed). Alternatively, allow break j and continue j to go with the @label.

[Edit: or in hindsight this doesn't looks so fantastic. Maybe better just to go with @label and @goto as they are.]

@rapus95
Copy link
Contributor

rapus95 commented Sep 19, 2019

I'm reopening because I think the break, continue idea is worthwhile. Syntax: zero or more break keywords separated by commas, followed by an optional continue; having a continue before a break makes no sense and should be a syntax error.

would this stop at function boundaries? Or could I escape a loop in the caller function? Not that it is a good idea, its just something that made me thinking 😀

@StefanKarpinski
Copy link
Member

would this stop at function boundaries?

Yes.

@GunnarFarneback
Copy link
Contributor

Proof of concept implementation as a macro in this gist for those who want to try it out.

Now available as the registered package Multibreak. The difference to the proposed break, break syntax is that the package uses semicolon instead of comma and requires attaching the @multibreak macro to an enclosing scope. Try it out when you run into situations where you'd want to break or continue out of multiple loops.

@jeff-e
Copy link

jeff-e commented Jan 14, 2020

Thanks for the working functionality, @GunnarFarneback.

Regarding the still-open issue for the core language, just a thought:

I looked at Go, Java, JavaScript, Perl, Dart, and modern Fortran for their multilevel break/continue done via labeling, as I'm sure is background for this thread.

I now understand the reluctance to adopt the same in Julia--because disappointingly (to me) all use goto-style labeling for break/continue. I say "goto-style" because all follow the ubiquitous LABEL: syntax indenting a code line or occupying its own line, shared by goto when present in the language (Go and Perl) with the exception of Fortran's numbered goto.

This tradition seems in part why people spoke of offering goto "instead of" labeled break/continue in Julia: "goto would be simpler and more general than break with labels"; and "I've never really cared much for the break LABEL and continue LABEL style. goto LABEL seems clearer to me." (Taken from the thread.) Under goto-style labeling, I must agree. But these quips never made full sense to me, because labeled break/continue when offered is supposed to be modern structured programming, thus incomparable to goto and independent of its offering. And certainly not conflated with it, despite that both require a labeling.

(Nothing against gotos.)

Maybe that's unfair ragging, so to the point: non-goto-style labeling for break/continue. Compare in Julia: continue labeled goto-style,

@label PROCESSING_LOOP
for i in I
  # …
  for j in J
    # …
    condition1 && continue PROCESSING_LOOP
    # …
  end
  # …
end

...vs. a more structured programming look like I'd expect,

for i in I labeled PROCESSING_LOOP
  # …
  for j in J
    # …
    condition1 && continue PROCESSING_LOOP
    # …
  end
  # …
end

Besides the unobtrusive readability, importantly the latter continue PROCESSING_LOOP tells unmistakably what to continue, not "where".

While of course doing so in slightly simpler reading than the goto workaround with label above the final end.

Note Python's PEP for multilevel break/continue had precisely the same thought, a postfixed labeling, which if adopted would have bucked the goto-style tradition above.

Goto-style labels have never been very satisfying for this... if we interpret "the statement immediately following" such a label as what to break/continue (not where), this antagonizes the label's other meaning as a code location for goto used traditionally. Which makes the resulting break/continue unnatural to read, as remarked in the above quips and this thread's 2014 discussion that led to its closing, dismissing labeled break/continue as little more than a limited goto syntax. Whereas the variant here clearly is not, as a modern-structured-programming structure independent of goto.

(Just as a bare single-level break/continue is not dismissed as a limited goto syntax.)

An alternative observation of this distinction from goto labeling, pre/post-fixing aside, is to simply note that the statement immediately following a goto's label can normally be anticipated to be swapped--throughout code development and maintenance as lines are inserted/removed there. So the label naturally occupies its own line. (What I've called a code location.) Whereas the new keyword attaches specifically to a statement, for reference to it. (This gives self-referencing code.)

Might I ask, @JeffBezanson, since you closed the issue back when labeled break/continue was the option on the table, before 2016's unlabeled variant led to reopening, can you confirm that the reasoning behind closing no longer quite applies here the same, no longer indicates closing the door on labeled break/continue?

I do not presume to abandon the 2016 unlabeled variant, before the two compete for adoption. Also I'm a nobody here, so I won't debate beyond this post.

Interestingly, Julia might be better positioned than most languages containing goto to adopt unique-from-goto labeling for break/continue, due to goto and its labels being relegated to macro-hood.

(To give credit where due, Fortran already achieved this separation actually, albeit slightly ironically (to me): break/continue labeling is done goto-style, as I call it, and goto labeling via old-school numbering. The former does employ a nice terminology though, "named constructs", which could be borrowed.)

On that note, distancing the new keyword from @label and "labeling" terminology might be desirable, I could understand, such as a keyword conjuring "named constructs" or whatever...

while delta > epsilon named GRADIENT_DESCENT
  # …

(using while this example for variety)

Now, the weighing of unlabeled vs. labeled syntax has not been taken up here, although I'll note once as others have that any break/continue by reference is arguably safer read/stated than the clever proposal of break, break, break, continue etc. Which I admit to admiring, besides the way its verbosity scales and besides the strain to human-parse it.

@StefanKarpinski
Copy link
Member

An interesting idea would be to consider the loop variable an implicit label for a loop. E.g. you could write break i and break the loop where i is the loop variable. This is complicated by destructuring like for (i, x) in enumerate(v) and such, but you could use i or x in such a case.

@GunnarFarneback
Copy link
Contributor

That opens up for interesting surprises if you carelessly switch the loop order.

From my point of view the main attraction of break, break is that it's entirely structural and doesn't involve searching for labels or other identifiers. Certainly, if the loops are sufficiently deep or convoluted there will be a point where unraveling the structure will be more challenging than following a label but don't let us be fooled by the generality of these constructions. I expect that the great majority of the the use cases will be break, break, a smaller minority break, continue and only a tiny fraction going deeper. For the truly complex cases I would be content with a standard @goto/@label. And I would consider refactoring the code.

@c42f
Copy link
Member

c42f commented Jan 15, 2020

That opens up for interesting surprises if you carelessly switch the loop order.

I wonder: are those cases any worse than the other kinds of order dependence which can make switching loops invalid? I would guess there's more insidious cases which already exist in normal julia code, such as push!ing elements into an ordered collection. In that case the loop variables aren't even mentioned.

@rapus95
Copy link
Contributor

rapus95 commented Jan 15, 2020

By which rules we determine the loop variables of while loops?

@StefanKarpinski
Copy link
Member

StefanKarpinski commented Jan 15, 2020

One of the things that bothers me about break, break and break, continue is that it appears more orthogonal than it actually is. The syntax suggests that continue, break and continue, continue should also make sense but they do not: it only makes sense to break or continue a specific loop that you're in. It would be more honest to write break@2 or continue@2 since the only degrees of freedom are wether you want to break or continue and which loop to break or continue.

@StefanKarpinski
Copy link
Member

By which rules we determine the loop variables of while loops?

Yeah, that's an issue—that isn't clear at all.

@rapus95
Copy link
Contributor

rapus95 commented Jan 16, 2020

Yeah, that's an issue—that isn't clear at all.

But for cartesian for loops that might be the perfect solution. Is it necessary to tackle all types of loops with the same solution? Because breaking intermediate cartesian for loops currently can only be done by splitting them up. Besides that in the case of cartesian for loops continuing a layer is equivalent of breaking the next inner layer.

@mschauer
Copy link
Contributor

As a baseline, @goto with suggestive choice of label names is a quite competitive status quo:

    for i in some_collection
        for j in another_collection
            println(i, " ", j)
            if condition()
                @goto continue_outer
            end
            if condition()
                @goto break_outer
            end
        end
        @label continue_outer
    end
    @label break_outer

@c42f
Copy link
Member

c42f commented Jan 17, 2020

@goto with suggestive choice of label names is a quite competitive status quo

I completely agree with this. In addition I find that many places where I might want this can be replaced with a little refactoring and an early return. So I feel like the bar should be fairly high for new syntax. In particular, introducing a keyword to label loops feels excessive.

Hence the idea of trying to use a macro to allow labeling loops in some way (eg, some variant of reusing the @label macro as I suggested above): it's a middle ground which doesn't introduce language syntax, but does allow things like while loops to be unambiguously labelled.

@c42f
Copy link
Member

c42f commented Jan 17, 2020

it's a middle ground which doesn't introduce language syntax, but does allow things like while loops to be unambiguously labelled

To expand on this: we could declare that

  • Unambiguous syntactic forms like for x in xs introduce a loop label x.
  • General ambiguous cases like while i < j can be manually labeled with something like @label myloop while i < j

This should allow continue x to work in simple obvious cases, and continue myloop to work in the general case without resorting to @goto. And it doesn't introduce any new keywords.

@DilumAluthge DilumAluthge removed this from the 1.x milestone Mar 13, 2022
@brenhinkeller brenhinkeller added the breaking This change will break code label Nov 19, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking This change will break code speculative Whether the change will be implemented is speculative
Projects
None yet
Development

No branches or pull requests