Pre-RFC: Break with value in for/while loops

Not entirely inconceivable. But that would be redundant to just writing the code after the loop, without any extra keywords. Someone who stops for a second to think about it would realise this. And the other obvious interpretation is the correct one.

The point is, that poll is just one data point (by the way, a nine years old one and by its own admission 'totally unscientific'), it's not so clear what factors had influenced it the most, and in Rust's case some of them could be mitigated.

3 Likes

What does break with value provide that just shoving it inside a closure? This looks like the definition of "syntactic sprinkles", you don't need it and it doesn't really make a difference to code anyway.

very contrived, but self contained example (unlike the OP).

fn main() {
    let temp: i32 = (|| -> i32 {
    let mut q = 0_i32;
    for x in 0..10 { 
        q += x;
        if q == 6_i32{
            return q;
        }
    } 
    return q;
    })();
    println!("example: {}", temp);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=a98e89ec9a6a1940e80d0997c8a1cbc3

Yeah I know it will only print out 6, but it is just an example. A closure is like 10 extra characters no matter the size of what is inside, and instead of breaks you can just use returns and it all still works.

Control flow cannot escape a closure, but it can escape a labelled block. Say, the ? operator can only bubble up to the closure call site; if you want to pass errors along out of the containing function, you’d have to use ? twice.

can you provide an example that illustrates this deficiency in the closure system?

Adapting your own example:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ba97c5977b528e792dc37efa2901e9cf

Okay I see your point

With labeled blocks, you can do

#![feature(label_break_value)]

fn main() {
    let temp: i32 = 'temp: {
    let mut q = 0_i32;
    for x in 0..10 { 
        q += x;
        if q == 6_i32{
            break 'temp q;
        }
    } 
    
    q
    };
    println!("example: {}", temp);
}

Or on stable,

fn main() {
    // abusing loop, even through there is no actual loop
    let temp: i32 = 'temp: loop {
    let mut q = 0_i32;
    for x in 0..10 { 
        q += x;
        if q == 6_i32{
            break 'temp q;
        }
    } 
    
    break q
    };
    println!("example: {}", temp);
}

With the help of a macro you can almost get the first version on stable. This doesn't have any of the issues a closure presents.

6 Likes

IMO Pre-RFC: Break with value in for/while loops is the only sane solution in the whole thread, yet it is not included in the poll. So I voted status-quo.

  • generator is the next best solution, but it doesn't cover while and are too experimental to rely on.
  • for/else or variants of it are hard to understand, as we've seen from Python. No, renaming else to then or finally doesn't help a bit.
  • changing the return type of for and while from () to Option<()>, breaking all existing code just for a niche feature? The cost doesn't cover the benefit at all. This is the worst solution among everything IMO.
    • if the type switches between () and Option<T> depending on whether break v; exists it is very ugly to explain.
    • retuning Option<T> instead of T also makes the behavior of break differ between for/while and loop.

The example written using the Default solution:

let result = while l<=r {
  let m = (l+r)/2;
  if a[m] < v {
    l = m+1
  } else if a[m] > v {
    r = m-1
  } else {
    break Some(m)
  }
}.or_else(|| {
  println!("Not found");
  None
});

(We could introduce a stricter trait such as ControlFlowDefault if Default is considered unsuitable.)

(And interestingly, with this feature, x.then(|| f()) maybe written as if x { Some(f()) }.)

4 Likes

I actually never read that comment until now.... I think it is a very interesting solution which does have some useful properties (which you mention).

I don't think it ever makes sense to impose general trait restriction (or even type restriction, in the case of the for-return-generator proposal) just because of (the flaw of) the syntax construct. A less confusing implementation for that idea would probably be to restrict (not infer) the type of the loop to be either () or Option.

But first, as you have said, the Option seems like a patch. Default that tries to 'unify' () and Option and even all the folks of types would be just a patch covering patches, effectively blocking the 'freedom of types' for those who don't want to, or even don't make sense to implement a Default.

Second, the users will probably never write break None because when they do, it is unlikely to make sense.

I believe this sort of problems are intrinsic and will keep poping up if the solution is to break down the original design, put the functionality inside, and then glue it back with however transparent glue, instead of to just discover what is already there.

I would say the naming is the only problem for the extra block. Someone proposed !break, which seems pretty accurate to me, but was rejected for the 'unconventional syntax'. But anyway, the extra block is not extra, and it means more than just an implementation of a random functionality.

Personally as a proponent of the generator approach I don't care about value breaks that much. In practice I can't remember many cases of when I needed such feature. But I really hope that integration of generators into for loops will help with the following pattern which is annoyingly common:

for value in iter {
    let value = value?;
    // ...
}

It even causes ad-hoc proposals like for val? in iter { .. } instead f a more generic solution. Allowing value breaks is just a nice bonus.

Note that iteration over Iterator<Item=Result<T, E>> has different semantics from iteration over Generator<Yield=T, Return=Result<(), E>>. In the former case you may receive Ok values after an error, but in the latter one the iteration must be terminated on a first error. (Unfortunately we do not enforce this constraint using type system, but it's a different discussion...)

I think if Python were granted another keyword, it would have no reason to use anything that deviates from its intention, namely: nobreak.

And as @felix.s has said, most of the unfortunate confusions from Python would be blocked simply by the type system of Rust. The kind of rhetorical questions like "why should I care about whether it is break-ed or not" is now legitimately answerable, since a type of a loop naturally implies a corresponding value at each possible exit of the loop, which is already part of the users' construction plan without any reminder from anyone, except perhaps from the compiler, in which case the error reflects not the flaw of the syntax but really the bug from the user.

Since the point is to return something from the loop, I would also propose the following syntax

for i in 0..n break None {
  if some_predicate(i) {break Some(i)}
}

// for generator:
for token in TOKEN_GENERATOR
  break |result| {
    println!("stopped scanning tokens");
    match result {
      Finished => println!("finished"),
      SyntaxError => println!("syntax error"),
    }
  }
{
  consume_token(token)
}

// backward compatibility
for i in 0..n /* break () */ {
  println!("{}", i)
}

Anyone who understands that the loop is not a statement but a value, which is already true, will intuitively get what it means even without referring to the documentation.

There is a trait restriction in the ? operator (Try), return type of main() (Termination), input of for (IntoIterator), input of match (StructuralPartialEq) etc. I don't see why we can't have a trait restriction on the input of break.

Nobody wants to unify () and Option in the Default solution, and there's no need to manually write break None at the end of the block, and also you can't use break; if the loop returns an Option.

The Default solution means that the while loop

while f() {
    if g() {
        break h();
    }
}

is de-sugared to

loop {
    if !f() {
        break Default::default(); // the only change
    }
    if g() {
        break h();
    }
}

You do need a default value when the loop exits without break, and Default does make sense in this context. If Default does not make sense for the type, that type should probably not be used for breaking.

Furthermore, since Option<T> implements Default even for non-Default T, you could simply write

let result = for x in it {
    break Some(f(x));
}.unwrap_or_else(|| g());
1 Like

? requires a Try-able thing, main requires Termination status, for requires an Interator (be it Into or not), match requires a StructuralPartialEq to find a slot for its argument. These restrictions are not really from the syntax but from the functionalities that they implement.

But, let's say for i32, why is its additive unit always more special than its multiplicative unit (or the other way around) ? Whatever <i32 as Default>::default() you choose, how should you know whether the loop has been broken or not in all the cases where the loop returns an i32?

What the loop should return in the case of being exhausted should really be the business of the loop, not of the type. Even if Default does not make sense for the type, that type could still make perfect sense in each specific realization of the loop, as well as of the type. The Default for all loops would be required only because of the lack of the syntax to specify the defaults for each loop. That is, Default would be required in this case solely because of the syntax instead of the functionality. A type level Default is not really as legit as the trait restrictions you have mentioned.

Yes I could indeed write that, just as I could replace all the for and while by loop. Without small enough operational, logical and aesthetic overhead, the reducibility itself does not really form an argument. When mutually reducible, I would prefer a solution without redundant Some, redundant unwrap_or_else and redundant ||.

3 Likes

I always find this pretty contrived. But perhaps if we could use the labels in a different way it would feel more natural. I am thinking on something such as

#![feature(yet_another_break_proposal)]
fn main() {
	let mut q = 0_i32;
	'myfor for x in 0..10 {
		q += x;
		if q == 6_i32{
			break q;
		}
	}
	arbitrary_middle_code();	
	if 'myfor broken as t{
		process(t);
	}else{
		something_else();
	}
}

This however would require a decent amount of new notation. And in contrast with Option or Default based solutions this averts the ending semicolon problem.

It is a pretty contrived use case in the first place.

Honestly, I find Yato's proposed workaround fairly elegant, given how niche a use case it covers. Its only drawback over the "break/else" version is to have an additional label, which isn't exactly the height of identifier pollution.

2 Likes

It is indeed an elegant workaround.

It might not be able to solve problems in situations that are not yet exist, like the adaption of for to Generator or a generalization of Iterator itself, but the labeled block is orthogonal to them anyway.

This is just as likely to be written as:

let result = while l<=r {
  let m = (l+r)/2;
  if a[m] < v {
    l = m+1
  } else if a[m] > v {
    r = m-1
  } else {
    break m
  }
};

println!("found at {}", m);
// oops, forgot the not found case

The billion dollar mistake strikes again. As I already said in so many words. Maybe I should have included it in the poll, but Default was neither very prominent nor particularly sane.

I don't think your solution deserves to be called more generic if it only caters to the case of Generator<Return=Result<_, _>> and badly, while actively hurting other possible use cases, like Generator<Return=!>. I actually wouldn't mind some kind of 'ad-hoc' proposal like for? here; if the problem is specific to Result, solve it in a way specific to Result.

In fact, let me sketch something like that here:

for? ITEM in GEN {
    BODY
} exhausted FINAL_VAL {
    FINISH
}

desugars to

let mut gen = GEN.into_generator();
loop {
    match gen.next() {
        Yield(ITEM) => BODY,
        Return(Err(e)) => Err(e)?,
        Return(Ok(FINAL_VAL)) => break FINISH,
    }
}

Bam. Now you can bubble up errors from Results without hassle, and the generator is still free to return something else entirely. What's not to like?

2 Likes

This proposal is equivalent to declaring myfor as an explicit Option<i32> variable.

fn main() {
    let mut q = 0_i32;
    let myfor = None;           // <----
    for x in 0..10 {
        q += x;
        if q == 6_i32 {
            myfor = Some(q);    // <----
            break;
        }
    }
    arbitrary_middle_code();
    if let Some(t) = myfor {    // <----
        process(t);
    } else {
        something_else();
    }
}
4 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.