For loop modifier blocks


#1

This is intended as a pre-RFC.

Currently for loops have four sections: initialization phase (A), iterate phase (B), check phase ©, and the body (D). They come together to form a standard for loop and is generally formed in a grouping similarly to the following:

           A
     | < - C < - |
 A'' |     D     |  A'
     |     B - > |
     | - > ... (following code)

A' : branch back to the check call
A'': branch if the check fails

My idea is add three new blocks: then, else, and between.

  1. The then block is entered when the for loop exits normally (the iteration runs to completion)
  2. The else block is entered if the for loop is break-en from
  3. The between block is entered after each iteration but not if the last check failed (so the iteration has finished)

Putting all of these together we could get something like the follow: with E as the then block, F as the else block, and G as the between block.

                          A
    | < - - - - - - - - - C
    |                     D < - |
A'' |       | < - - - - - B     | B'
    |       |       | < - C     |
    |  B''  |    C' |     G - > |
    | - - - | - - - | - > E - - > |
            | - - - - - > F       | A'
                          ... < - |

A' : This is the branch to skip over the `else` block 
A'': This is to go to the `then` block if the first iteration fails
B' : This is the branch to continue the loop after the iteration and the between block
B'': This is the break branch to go to the else block
C' : This is the branch when the usually check fails to go to the else block

How does this sound? I know that it could be complicated but other languages have at least some of these things so I don’t think that it is completely unreasonable to have.


#2

The “else block” is probably the most useful of these, but it can already be expressed in other, not very convoluted ways (e.g. I would probably suggest extracting the loop into a function or closure and just returning instead of breaking). This suggestion and similar ones come up every once in a while, because Python, but the general assessment seems to be that the added complexity/confusion/ambiguity is not worth the occasional usefulness (with which I agree), see e.g. this Reddit post.


#3

And note that Python’s else is the opposite of this proposal. (It is entered if the loop does not break.)


#4

Various subsets of this have been suggested, and are often met with fierce opposition. Some points specific to this proposal:

  • Python’s else is your then. (not that this should stop us. I like your naming better)
  • Some use cases for the else block require access to locals from the for body. This will require declaring an uninitialized variable outside the loop (slightly awkward), or letting break take an argument that is passed to the else block somehow (sounds complicated designwise but IIRC one of the past suggestions was in fact centered around such an idea).
  • I like the between block.
  • That control flow ASCII diagram is terrifying. I would suggest not including it in an RFC.

#5

Actually yea, this was my thought: can we revise to have an example that isn’t so spooky?

I’ve given it all a read and I think we could very much benefit from a pseudo-Rust snippet showing off how this would be used in every-day code, so that people who find it hard to imagine can play it out in their mind.


#6

Note that none of these are unique to for; I’d like to have them for while too. Obviously, none of these make any sense for loop, which can only be exited by breaking.


#7

Sure, here is some rust about how it might work.

for i in 1..10 {
    print!("{}", i);
} between {
    print!(", ");
}

Output: "1, 2, 3, 4, 5, 6, 7, 8, 9"


let buf = vec![];
...
for i in buf.iter() {
    if (!process(&i)) {
        break;
    }
} then {
    wrap_up();
} else {
    produce_error(&i);
    exit();
}

Workings: This loop would go through every item in the vec and then if the processing
    fails then do something and exit otherwise do some wrapping up.

I know that these are not the best examples since they could be rearranged to work but I think that it is a start.


#8

Actually that’s great! Thanks!


#9

I just checked the grammar- unfortunately, neither then or between can be mere contextual keywords, since this compiles today:

struct then {}
for _ in 0..1 {} then {}

We have two options: use existing reserved keywords, wait until another round of keyword additions arrives, or something horrible like

for _ in 0..1  {

} then do {

}

#10

To be fair I don’t think that last solution is that horrible but I would agree that it is sub optimal. I don’t think that something like this is worthy to wait until we can reserve new keywords though


#11

This makes what you’re looking for much clearer.

However, I feel like these are sufficiently uncomon loop constructs that it’d make sense to start out implementing them as macros and see if they end up getting widely used. I don’t know of any precedent for these other than Python’s else on for loops.

Do you know of any precedent for between?


#12

Sure I can look into making macros for these.

For between the precedent mainly revolves around rust’s general inclination to promote the use of iterators. When using c style for loops you can do a temporary increment and check yourself.

Therefore it is possible with those loops but prone to maintainence fatigue since any change to the loop check how has to be checked twice.

However, with iterator loops this is almost impossible. The for-loop example I gave above. You would have to produce a separate variable for the current index and then check that verses the length. But that does not translate to non-ordered structs very well.


#13

I figured that if you were to have Python style else we might as well have the opposite.


#14

Take a look at this proposal, which is somewhat alternative to that you propose:


#15

Thank you for linking that proposal, it is indeed a somewhat alternative to what I am proposing. I think that the generator argument is a very strong one for these sorts of blocks since it is impossible in a for loop to know if you are in the last iteration.


#16

What do you mean by a “non-ordered struct”? And how would something not work with them? Do you mean iterators with no known length/iteration count? How do you expect such iterators to be able to tell whether they are at the last or penultimate iteration with the help of any sort of syntactic sugar? That would require predicting their future state which is just as impossible with between blocks as without them.


#17

I mean structs that don’t have a definite order, a vector does since it is like an array but a hash map does not necessarily have a well defined order that you can test given the current item. Of course you could also get the “index” and the count within the map but I am trying to avoid having to do that because it is messy because that piece of information is not really pertinent to the computation.


#18

between can be implemented to run before each iteration except the first. Then you just need a single boolean flag that gets set after the first iteration.


#19

Yes that is very true, but it leads to many more variables concerned strictly about control flow and not data, in a sense flag variables. Which is not often good software design, that is why I am proposing block level scoped control flow.


#20

Ah, so you mean an unordered data structure, fair enough. I still don’t think avoiding .enumerate() and index < the_hash_map.len() - 1 warrants complicating the core language, though. It’s an uncommon enough case to be served well enough by this idiom.