That's not how I thought it would work, I exected that continue
would pass an argument to resume
and if you omit continue you get continue ()
at the end of the loop. Although this does require generator with resume arguments. Then break
would have to match the type of Generator::Return
. But this shouldn't pose a problem because we would presumably have a combinator for mapping the return of the generator, i.e. map_return
, so we could force the return type to be whatever we want.
That is actually I would have expected such things to work. continue
for resume arguments makes more sense than break
for item in haystack {
if is_needle(item) {
break item;
}
}
is the go-to (ahem) example. You may argue it's silly given that find
already exists, but then your own proposal can also surely be re-expressed with a combinator.
That seems like a workaround, not a fix. If it were really the case that for
loops fit generators that return values well, a natural syntax to handle them would fall out of that proposal, without resorting to combinators. If you're not above using combinators, why not express the loop itself with a combinator?
Consider: a loop
can evaluate to any type the loop body chooses. An if
-else
expression can evaluate to any type its bodies choose. But the for
loop's return type is supposed to be chosen by the generator, why? And the generator proposal's definition of how break
is supposed to work is so confusing and unnatural, that @newpavlov who proposed it cannot remember it!
As alternative to the proposed evaluating for
to either ()
or Some(T)
depending on the break t
, as both types implement the Default
trait:
- If there is a
break t
in thefor
then it should evaluate to the type of t. - If the
for
can end without abreak t
statement (is this always true?), then the type must implementDefault
, whose default value is used. - If there are non-agreeing
break
s then it is a syntax error. - If there is no
break
element we advise type inference to be()
, but it could be anyDefault
. E.g., with afor
being assigned into a variable and then removing all itsbreak
statements.
This should be completely compatible with existent code. I have no idea if it would imply some notable change in the compiler. Does the compiler have a notion of the Default
trait?
It's mainly just that its inconsistent and needs to be explained to everyone. It's not the same as loops evaluating to T
or ()
, because break;
is regarded as equivalent to break ();
, just like return
is. In order for it to be the same, normal loops would have to evaluate to Option<()>
.
And that's the only "practical" example which you can provide? Then I am even more sure that this feature does not pull it's own weight in the proposed else
form.
Please tone down your attacks a bit. Firstly, I wasn't proposing this feature (re-read my previous message carefully), I've just mentioned that I vaguely remember that people have proposed something like this in my proposal which does not include any modifications of continue
behavior compared to today. Secondly, it was a late night for me when I was writing that message. And thirdly, as I've written several times already, I don't think this feature pulls its weight in both discussed forms, although I think the generator one is a much more natural one. It fits nicely into a notion that iterator is a coroutine with both return and resume argument types equal to ()
and generator is a coroutine with a resume argument type equal to ()
. And it's fully backwards compatible with how for
loops work today, since for
loop always evaluates to ()
and loop body has to evaluate to ()
as well.
In other words, with the generator/coroutine integration we will be able to write code like this:
// gen has type Generator<Yield=u8, Return=Result<(), &str>, Resume=&str>
// but I think coroutine will be a better name for a generator with
// resume arguments
let gen = ...;
let res = for byte in gen {
match byte {
0 => break Err("got zero"),
1..10 => continue "less than 10",
_ => (),
}
// do stuff
"loop end"
};
Not a fan of this, for the following reasons:
Default
is a bad idea on its own meritsDefault::default()
can potentially have side effects, which would not be apparent from the loop code- Your proposal fails to distinguish exhausting the loop from
break Default::default()
- Evaluating to
Option<_>
is no less expressive, as you can still do.or_default()
Well, you did post it as a pre-RFC, and it did include break
semantics (which you got confused with someone else's continue
proposal). I don't like the continue
proposal either, but for different reasons.
Also, consider that with the generator proposal, if you have a generator which returns values indefinitely, i.e. a Generator<Return=!>
, then a for
looping over it cannot meaningfully use break
at all (unless it's out of an outer block). I'd argue that breaking out of an infinite loop is a quite important operation that shouldn't be too awkward to express.
I do not think that is true. Perhaps it is easy to misuse. I do not think this is the place to discuss if it is correct to have a Default
trait in rust.
A hidden call to default
could certainly be problematic and had not thought of its implications. However, is there any real code in which a spurious call to default
causes a problem?
That is the wrong perspective. You write break Default::default()
only when you do not want to differentiate that situation from normally ending the loop. Perhaps it is a sorted list and you have already found a value greater than the you are searching and you can stop the loop immediately without providing a value.
Yes. The improvement over Option
is to never have confusion between returning ()
or Option<()>
.
Consider the code
let x:Option<usize>=for i in 0..10
{
if some_condition(i) { break i;}
some_other_thing(i);
};
Then if we remove the break i
the type would change from Option<usize>
to ()
. I suppose you could also apply inference on the absence of break i
, but it seems odd. In my opinion break Some(i)
makes it more clear.
I also didn't propose the final expression being a resulting value of a for/while loop either (see "loop end"
).
Surely you wouldn't explicitly write break Default::default()
under this proposal unless you wanted to conflate the two cases, but it may be the case that you do break val
where val
happens to be the same as what Default::default()
evaluates to, and later code mistakes that value for the case that the loop was exhausted. This is precisely why we have discriminated unions in the first place.
Or it could be resolved by making for
loops always evaluate to an Option<_>
. It will break compatibility, but that can be easily handled with an edition change.
I'd argue that such cases will be extremely rare and they can be solved (or "worked around", does not matter) well enough with a combinator changing return type of a generator. For example we could have a combinator which will map return type of a generator from T
to Option<T>
, so you will be able to terminate loops over an infinite generator with break None;
. Also this solution will work without any issues in generic contexts as well, when T
is not known.
I guess my thoughts are it is trivial to implement impl Into () for Option<()>
how terrible would it be to automatically call into() when the expected result of the for loop is of type (), and the for loop returns Option<()>
?
I mean it seems like it would be backwards compatible, and if you want to preserve the difference between Some(())
and None
, you probably wouldn't be converting it to unit.
Perhaps a terrible idea, and a suggestion somewhat out of character as I generally do care about preserving type information, but find it hard to care in this specific situation.
edit: I'm guessing that perhaps there is some case involving generics where this wouldn't actually work. actually since into is reflexive it should work, but it seems we would have to always emit into() for for loops, which sounds less desirable to me. shrug
I've written this already but in a way that involved a lot of jargon, so I want to be clear about the consequences of a change like that.
Any time you write a function without a return type that ends in a for
or while
loop, it would now need to be followed by a semicolon. For example, this function would need to be written like this:
fn main() {
let mut listener = TcpListener::bind(env!("SOCKET_ADDR")).unwrap();
for stream in listener.incoming() {
handle_connection(stream);
}; // <- note the semicolon!
}
This is of course possible with an edition change, and even easy to autofix, but is it desirable? I really don't think so: it's a nice property of the way Rust's expression/semicolon system has worked out that block expressions normally do not need to be semicolon terminated.
I think that it goes a bit farther than that,
Every time you have a for loop followed by another expression the for loop would have to be terminated by a semicolon.
for × in iter {
break 0
}; // <---
let x = 0; // any other expression
Similar to how match that yields a (edit: non-unit) value must be terminated by a semicolon
match () {
() => 0
}; // <---
let x = 0;
Generator<Return=!>
is a quite natural way to model an endless counter or an event source. Making it awkward to break an loop based on it will make generators a much less attractive feature.
You keep bringing up examples with generators returning Result<_, E>
. But what if the loop body wants to raise an error not covered by E
? (For example, the loop body receives a raw packet of data from the generator and wants to raise a ParseError
, as opposed to an io::Error
that the original generator can raise.) You'd have to keep using combinators just to to extend the range of possible errors; in other words, you'd be fighting the type checker instead of working with it. This design doesn't work all that well even for your own use case, never mind others.
And like I said before, if you don't mind adding combinators, you might as well add an iteration combinator instead of extending the semantics of the for
loop.
I think a more expressive language is worth the price of occasionally having to type a seemingly superfluous semicolon. It's just syntax after all. If I cannot convince you of this, then I believe there is no point in continuing this line of discussion.
(Pedantic point: every match
yields a value, and every function has a return type. Sometimes the type is ()
, but that's still a type like any other.)
The point here is not really about the cost of typing a semicolon (which I agree is not a compelling point on its own), but about the cost of breaking changes: having to "fix" all the existing Rust code that today doesn't have these semicolons, updating all the tooling/docs in the ecosystem for these new rules, and having to re-teach all Rust users what the semicolon rules are.
Editions do make breaking changes possible, but they don't change the fact that the bar for breaking changes is (and should be) very, very high. The motivation for this change is just nowhere near strong enough.
I really don't think that you will have reteach the semicolon rules. Since they won't have changed.
I do, however, submit to the fact that forcing all for/while loops to have a semi-colon after them is a very large change.
Though I have not yet been convinced that having the type of a for/while loop change from ()
to Option<T>
if there exists a break T
expression in the loop body constitutes a drastic enough oddity to not warrant perusing that direction.
Exactly, and I would even say it makes sense.
for x in data {
...
if ... {
break; // very clear that we don't care about the for-loop as an expression
}
}
let val = for x in data {
...
if ... {
break found; // very clear that we want the value, from here and from the `let val`
}
}
I voiced above that break;
interchangeable with break ();
seems to have been taken as an axiom in past discussions of this feature, but isn't actually valuable. I would much rather see break ();
explicitly if someone wants a loop that evaluates to Some(())
.
I very strongly agree. Since the empty tuple would then be the idiomatic way of getting else/then functionality without new keywords
I disagree, it makes the language more consistent, which is valuable. It makes Rust easier to teach/learn, to write and audit unsafe
code, and to write macros. The last point especially would be hurt if we change the meaning of break
to be different from break ()
, as we would lose the correlation to return
, where return
is exactly the same as return ()
. (it makes it harder to abstract over break
and return
). But I do note that this isn't an important point on it's own.
Breaking consistency should have a high bar. I don't think that break
changing meaning is sufficiently motivated. I would find it surprising if break value
(where value: T
) yielded a Option<T>
I also find the motivation of find a value using a for
loop to be poorly motivated given that you could just use Iterator::find
, or if you have strange control flow, use Iterator::try_fold
, or if you have really strange control flow, then you are probably not going to benefit from making for
or you are going to be using loop
instead of for
anyways to signify the strange control flow.
I think we can special case !
here, so that we can just break
with a value, and have it do the right thing. !
is already very special, so I don't think that this would be that big of an extension.