JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.43k stars 5.45k forks source link

for-else and while-else #1289

Open StefanKarpinski opened 11 years ago

StefanKarpinski commented 11 years ago

Allow and else clause on for and while loops that executes if the loop never executes.

quinnj commented 10 years ago

Bump. Can this be 0.4?

mlhetland commented 10 years ago

I guess you've decided on this behavior—just thought I'd pipe in with an opinion. I find Python's behavior really useful here, where the else clause is executed if the loop has not been terminated early by break. The assumption is that you're looking for something, and the else clause is for if you don't find it. Maybe less seemingly logical than the suggested behavior, but I've had use for it many times, at least. In Python, it's also consistent with the else clauses when catching exceptions (try/catch/else), where the else clause is executed if there was no exception. The two seem quite analogous, and the else clause for try statements is also something I think would be really useful. I guess one could still have that even with the suggested behavior here, of course.

StefanKarpinski commented 10 years ago

That Python for-else behavior has always struck me as very unintuitive, honestly. Mostly it's the name, not the functionality itself. The functionality can be emulated with gotos, but it is kind of awkward.

elextr commented 10 years ago

Whilst there is nothing against this, at least in my experience the thing that is needed most often is to know if the loop was breaked (broken?) or if it completed.

But I have never found a "nice" way of specifying the required combination(s) of the three conditions, that the loop never ran, shortcut (break), or completed.

mlhetland commented 10 years ago

Yeah, @StefanKarpinski, I guess it's mainly a matter of recycling keywords in Python. I think the naming is less awkward/surprising for try, though, if you see it as an alternative to the catch part. But, sure, the naming is more logical in the original feature spec. I can't recall having needed that functionality, but if it existed, I surely would ;-)

ivarne commented 10 years ago

Two different suggestions for what one rarely used syntax should mean, seems like a bad sign.

StefanKarpinski commented 10 years ago

Well, one would never guess at the Python meaning without already knowing it, whereas I think the meaning of executing if the main body never executes is pretty intuitive, but yes, that's still a point.

prcastro commented 10 years ago

I know that it's a keyword not yet used on Julia, but I feel that the then keyword is more intuitive when doing what @mlhetland suggested. What do you think?

mlhetland commented 10 years ago

Actually, @StefanKarpinski, I hadn't noticed there were gotos in Julia; saw them in the NEWS.mdfile for 0.3.0 now. Good enough for me—just a @goto instead of a break, and a @label instead of an else (though placed differently, of course). Better than, say, lots of Boolean flags and conditionals. Speaking of macros (and loops), would it be totally insane to adapt something like the Lisp LOOP macro for Julia? (Possibly in a separate library.) Not sure how useful it'd be. Just saw it mentioned in a (much-publicized) blog post on Julia vs Lisp, and implementing it (or something like it) sounded like fun ;-)

wizzat commented 9 years ago

This is a super useful feature. Has a resolution for this been decided or is everyone going to use goto?

tonyhffong commented 9 years ago

using @goto will trigger a lint warning, fyi.

ivarne commented 9 years ago

@tonyhffong Why?

tonyhffong commented 9 years ago

er, I thought the whole point of for-loop and while-loop is to minimize the use of goto. Julia allows goto, that's fine, lint.jl doesn't have have a big-boy assumption.

ivarne commented 9 years ago

TL;DR; I think an unconditional warning about goto/label belongs in a style checker, not in a linter.

I thought the point of the higher level constructs was to be able to express most code in a clearer more readable way. We had a thorough discussion before adding goto and found it to be better for some usages. Wikipedia defines lint as "tools that flag suspicious usage in software", and I don't think @goto and @label quailifies as suspicious unconditionally. If you could warn just for cases where it would be trivial to rewrite to a while-loop (or other constructs), it would be great and within the scope of a linter!

wizzat commented 9 years ago

I would say that people are much more likely to do something like this:

found = false
for x in 1:5
    if x == 3
        found=true
    end
end
if !found
    //error handling
end

than this:

for x in 1:5
    if x == 3
        @goto found
    end
end
//error handling
@label found

for/else and while/else are the natural and obvious solution, even if you rename the else clause. I saw a talk from a core python dev who wished they'd named "else" to be "nobreak". If we're so concerned about naming, perhaps nobreak is a good name. At least it's descriptive. (But for/else actually has a history in CS so it shouldn't really be so surprising to people...)

tonyhffong commented 9 years ago

@ivarne it's a grey area no? We judge "poor style" being one that can be prone to logical error. So where is the line between "prone to error" (lint ok) and "suspicious usage" (lint not ok)? Of course goto is not suspicious unconditionally, so are many current lint warnings, such as declaring an unused variable, dead branch, empty literal range [1:0]. This criterion seems inadequate. There's always judgment involved. Well currently it's mine, but it could be changed.

How about this, I could add lintpragma to silence the goto lint messages (btw, the current message is only at the INFO level, not at the higher WARN/ ERROR/ FATAL levels). Do you consider this meeting half-way?

ivarne commented 9 years ago

The discussion about lint and @goto might continue in https://github.com/tonyhffong/Lint.jl/issues/33

Please continue the for - else discussion here.

cossio commented 8 years ago

I prefer Python's behavior too. It's the one I use most often.

2Cubed commented 7 years ago

I think the else name makes sense, when thought about in the proper context. "loop, continue if 'successfully exited' (break), else run [block]."

This feature is quite helpful in Python, and I would love to see it in Julia, as well. :slightly_smiling_face:

StefanKarpinski commented 7 years ago

But for/else actually has a history in CS so it shouldn't really be so surprising to people...

Is there actually precedent for this construct in any language besides Python?

Liso77 commented 6 years ago

IMHO for-then-otherwise could be better than for-else because it is more complete and won't create confusion for pythonistas.

I propose also thinking about other use of for. For example:

[i for i in A then in B otherwise in C]

But I understand that then there could be questions/complications:

# what if cond(i) is false for every i in A? Does it invoke otherwise clause? 
 [i for i in A if cond(i) then in B otherwise in C]  
# inner loop only after then/otherwise?
[z for i in A then in B otherwise in C for z in [7*i] ]  
stords commented 5 years ago

I know I'm reviving a super old thread, but I have been really missing this kind of syntax recently. What do people think about using the word "finally" ? Code in the finally clause executes if the loop ends successfully, and not if the loop is broken out of.

Its not 100% in line with the use in a try-finally context, but I think the word still makes sense, and would be appreciated by people used to the while-else syntax.

cossio commented 5 years ago

To be honest I no longer miss the for -- else thing (with the Python's meaning of executing the else if the loop is broken). I think I missed it before because I was too used to using Python, but now Julia has detoxified me ;)

See also https://github.com/JuliaLang/julia/issues/22891 for a more complete proposal.

However, in response to that thread, I would say that a function is perfectly fine for this (I post here to not revive that old thread):

function search_prime(values)
   for x in values
      isprime(x) && return x
   end
   return maximum(values) # makes no sense, but whatever
end

So, is there any good reason to add this feature? Like any real use case where it would really simplify code?

StefanKarpinski commented 5 years ago

I written a fair amount of code recently that could really use the for else feature.

quinnj commented 5 years ago

I've found myself wanting this recently as well.

jeff-e commented 5 years ago

If you'll indulge me for a moment recapping this feature from my point of view, I can possibly add a note about scope.

All loop exits but the normal one have "instructions on exit" expressiveness, due to the embedded nature of breaks, so the new feature just evens the score:

for i in I # or while statement
  # …
  if exit_condition
    # …break's exiting instructions here…
    break
  end
  # …
  if exit_condition2
    # …2nd break's exiting instructions here…
    break
  end
  # …
then
  # …normal exiting instructions here…
end

...where I've used stand-in keyword then for the discussion and where break breaks out of the entire "loop-then" as expected.

Which is a quite general bit of control flow to leave out of a language.

Besides the obvious expressivity gain that Python gains from too, avoiding the need for boolean flags / other workarounds as usual for an omitted control flow structure, in Julia there is one further privilege of exiting instructions that is not expressible beyond the loop: loop scope.

Thus even in a loop absent any breaks one might choose to use then before giving up scope.

Am I mistaken?

This makes keyword else, already acknowledged as somewhat awkward, if acceptable, (and in some usage patterns readable), particularly awkward when used without break.

@stords:

Unfortunately, finally seems to be in spirit somewhat opposite what is needed, in its role providing exiting instructions regardless of how a block exited.

As an aside, if one really wanted to, finally could be given to loops to mean just that, "no matter how exited" (rather than the current error), appearing after then when both occur, and would merely allow code deduplication when all exits share common (scoped) code. But that is orthogonal to the question here, and I don't promote it specifically.

As to the painful situation of not obtaining the then feature until 2.0, I would say:

While finishing 1.0 the devs chose to kick the can on this til 2.0. Now we live with the wait. (Or in the meantime, are tempted to expropriate else whether compromisingly or not in the long term.) I suggest seeing it as another goodie awaiting us in 2.0 of which there must be many, the more of which will usher it in sooner.

Quick question @StefanKarpinski, I believe your most recent post refers to Python's meaning of else (and not to your OP meaning). If this github issue is now dedicated to that feature and not to the OP, including for milestoning, would you find it advisable to change the title to match? (Which is not characterized by else per se, though it may end up solved by else.) Just a question. Of course if you still want to consider the functionality of your OP, then this sidetrack we find ourselves in should perhaps all branch to a new issue.

StefanKarpinski commented 5 years ago

Thus even in a loop absent any breaks one might choose to use then before giving up scope.

I'm not following what you're saying here about scope. Can you give an example to illustrate?

If this github issue is now dedicated to that feature and not to the OP, including for milestoning, would you find it advisable to change the title to match?

Yes, we're only considering the Python-like meaning at this point. The title seems to still be accurate, however, since the feature is applicable to both for and while loops as far as I can tell.

As to the painful situation of not obtaining the then feature until 2.0, I would say: While finishing 1.0 the devs chose to kick the can on this til 2.0. Now we live with the wait.

for-else is currently a syntax error, so there's no need to wait until 2.0 for this feature. I find the statement of how hard it is to live without this feature a bit overstated. Many languages, including C, C++, Java and JavaScript have gotten by without this for their entire existence and even become respectably popular.

jeff-e commented 5 years ago

I'm not following what you're saying here about scope. Can you give an example to illustrate?

Thanks, I guess I should've given an example.

Consider finding and storing the largest power of 2 less than a million. One might write

n = 2^0
while n < 1000000
  local old_n = n
  n = 2n
then
  n = old_n
end

(We could divide the final n by 2 instead of what's done here, but in an analogous example that might not be possible.)

Keyword else in this situation might be a bit bizarre.

I don't mean to suggest there aren't alternatives to the coding approach here; but rather, I mean this is a reasonable usage of the features of Julia once the new feature is adopted. So we can expect its use.

A keyword along the lines of then, not mentioned first by me, handles all use cases naturally.

The title seems to still be accurate, however, since the feature is applicable to both for and while loops as far as I can tell.

I was actually not referring to your use of for and while in the title, but to your use of else in the title (which presupposes the feature's syntax).

But no worries about the title, it's a title.

I find the statement of how hard it is to live without this feature a bit overstated. Many languages, including C, C++, Java and JavaScript have gotten by without this for their entire existence and even become respectably popular.

Acknowledged. Apologies for overstating this.

KevOrr commented 5 years ago

Just wanted to point out that @StefanKarpinski's proposal is not inconsistent with the Python sense of for-else and while-else. For example (note how range(3) is changed to range(0) in the third loop):

In [1]: for i in range(3): 
   ...:     print(f'in loop. i = {i}') 
   ...: else: 
   ...:     print('in else') 
   ...:                                                                                                  
in loop. i = 0
in loop. i = 1
in loop. i = 2
in else

In [2]: for i in range(3): 
   ...:     print(f'in loop. i = {i}') 
   ...:     break 
   ...: else: 
   ...:     print('in else') 
   ...:                                                                                                  
in loop. i = 0

In [3]: for i in range(0): 
   ...:     print(f'in loop. i = {i}') 
   ...:     break 
   ...: else: 
   ...:     print('in else') 
   ...:                                                                                                  
in else

By adopting the python sense of for-else and while-else (or whatever else should be called), both proposals in this thread are satisfied

mlhetland commented 4 years ago

That depends on how you read the proposal. At least to me,

Allow and else clause on for and while loops that executes if the loop never executes.

seems to be using a definitional if (see, e.g., p. 123 here), i.e., to define an else clause that is defined to execute exactly when (i.e., «if and only if») the loop never executes. That seems to be the point of it, really; «either execute the loop, or do this other thing» (though I may, of course, be misreading his intention…). And that's incompatible with Python's version (e.g., your first example).

I still regularly need the Python version (and have never so far needed the one suggested here) … but that's life :-}