Break with value alternatives

This is because an empty match evaluates to the never type, my point still stands.

return already evaluates to !, this is why the try macro works.

I'm going to continue this on a PM so that we don't pollute this thread some more until we reach a consensus. Is this fine with you?

Moderator note: @Yato and @felix.s later agreed that, while the conversation needed to be split into its own thread, it did not need to be a PM. The contents of the PM thread are shown below:

This is uncalled for. I do understand type theory, and uninhabited types would work seemlessly with the generator solution.

After think about it a bit more, we don't actually need to special case based on inhabitedness. It would just fall out of the generator desugaring.

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

This should just fall out of the generator desugaring I showed above.

Yes, this is what I mean as well

I don't know about this. Iterator is pretty useless without combinators. (You can't easily iterate over two things in lock step). Also chaining iterators is now impossible, and oh look, fairly useless iterators. At this point iterators are only useful as a minor performance improvement. (While this may be enough, it isn't what makes Iterator the perfectly designed trait that it is)

The compiler can reason about inhabitedness so you don't need to.

Regarding empty matches evaluating to !, which you mentioned in your last post, I think before ! was introduced as a first-class type, match x {} evaluated to whatever type inference told it to. Though I don't really have a way to check if that's still true now (or was back then). And yes, return's type was () before first-class ! was introduced. I recall compiler people sighing with relief when they could finally remove special handling of unreachable code that was necessary because of that.

I was referring to people being confused over whether () is a top type (which it obviously isn't). I don't even remember if you participated in that issue's thread, to be honest.

Ouch. I mean, yes, it compiles… but you're relying on for being a sugar for loop-match and on the break from the generator implicitly converting the ! return value to the other break's type. If for were a primitive construct which actually enforces that break agree with the return type of the generator (as a good abstraction should behave), it would not work. I think it's a deficiency of this desugaring that it does.

Sure you can chain Iterators with just for, you just write two loops. Of course combinators make it more convenient, but they don't interfere with the usefulness of for for iteration on its own; for still does its job even if you don't have combinators. To contrast, the break semantics from the generator proposal diminish its usefulness for returning values from the loop quite significantly.

As the cliché goes, code is written for humans to understand and only occasionally for machines to execute. When reading code, I want to think about for as a black box most of the time, without adding its desugaring to the cognitive burden of understanding it.

On the note of Error type in Result. I see this as exactly the same problem as implementing a trait, say Write, and wanting to provide more meaningful errors. You just can't without some workarounds. Also, I don't think that this is a common issue, so I'm fine with pessimizing it.

The compiler already has to do inhabitedness checking, so this should be fine.

Also on the note of inhabitedness, in the past there have been miscompiles when using structs that contain uninhabited types because you could do something like,

So now, all structs treat uninhabited types as normal zero-sized types, and don't inherit inhabited. Also partial initialization is prohibited. https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=36bdbea07f53477015b7c0ce9a26e120

So type theory, while nice, doesn't completely line up with Rust.

I don't remember that thread at all, so probably not.

In case you didn't know, this is how for loop desugars right now,

for $var in $into_iter {
    $code
}
let __iterator = IntoIterator::into_iter($into_iter);

loop {
    match Iterator::next(&mut __iterator) {
        Some($var) => { $code },
        None => break
    }
}

Please show how your method would desugar a for loop, because if it can't then it is more complex then generators

for loops are not primitive constructs in Rust.

You can't extend the error type returned from Write, because you're coding against an abstraction designed by someone else, on which outside code may be relying. This doesn't apply to for loops: if I write the loop, I also write the code which receives the value returned from the loop. If I can choose what value it is, why am I not allowed to also freely choose its type?

I've seen that issue, and I don't see how it's relevant here.

'$label: for $var in $into_iter {
    $code
}

can be desugared into

{
    let __iterator = IntoIterator::into_iter($into_iter);

    '__label: loop {
        match Iterator::next(&mut __iterator) {
            Some($var) => $code,
            None => break None
        }
    }
}

where the free occurrences of break $val and of break '$label $val within $code desugar to break '__label Some($val), and those of plain break and break '$label to break '__label Some(()). If we go with the backwards-compatible variant, only loops that contain break with a value desugar in this way, and loops with only plain break desugar as before. Are you satisfied with this description?

I just saw this, do you know if there is a way to change a PM to a new thread? I think that would work better. Sorry about any possible confusion on this front.

Yes, this looks fine.

To me, having a feature live as a library as much as possible is more enticing than having it baked into the compiler. Given that we would want generators anyways as a means for co-routines, I think that extending for loops to handle generators would be nicer.

How would this work under your desugaring? Will it be a type-based desugaring?

1 Like

I don't think so. Oh well, we shall cope.

I keep saying iteration can also be a combinator…

I think @canndrew's for-else proposal I linked to earlier (with the else path receiving the generator's return value) is a much better design (except for continue); in fact, I was thinking to suggest something similar as an alternative before I saw his post. It's just a matter of choosing the actual keyword to use. And it's not even incompatible with the Option<_> proposal.

I just described it to you as the ‚Äėbackwards-compatible‚Äô variant; no type sniffing necessary, just peek into the loop body to check if it contains a break with a value and choose the specific desugaring based on that. It's purely syntax-based. (I mean, type checking is also technically syntax, but‚Ķ)

Also, since you care so much about desugarings and suggested adding inhabitedness checking to the generator proposal, I'll note that there's no way to express inhabitants checking in user code currently, so that variant cannot be desugared.

This is the first that I saw you say this,

It looks like you are referring to this from the main thread

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?

And yes, we could implement iteration as a combinator, and ideally that would be the end of it. But a lot of times when you are using explicit loops, you want to be able to return early from the enclosing function. This is hard to do with a combinator, because by their nature can't access the control flow of the enclosing function.

We could do this similar to the current Iterator::try_fold mechanism, but for generators.

Ok, that seems reasonable.

I'm not adding anything new, just reusing existing machinery.

I provided the desugaring, so...

The theoretical inhabitedness checking of more exotic uninhabited types is off for future work. It currently doesn't work with any other part of the language, so I don't see a reason to add it here.

Yes, that is true. Though I've seen a proposal (from @josh if I remember correctly) introducing macros with method-like syntax. If combinators were implemented as those instead, they would be transparent to control flow constructs and we wouldn't have this problem.

Sometimes I pity how Rust's design seems to have been rushed without considering whether a feature could be made more general. But then, there's no way the 1.0 release could have been kept being postponed indefinitely…

1 Like

One problem with the macro solution is that it will produce sub-optimal code for chain and possibly for zip, because it can't hoist checks out of the loop, so there are tradeoffs everywhere.

With try_fold you can break out of the loop early, but it makes returning early from a function more confusing than it needs to be.

1 Like

That is one of my qualms with the strict comparison to combinators and iterators. It doesn't compose well with early returns at all. I find that I generally program with both. I build up an iterator and then loop over it in a for loop. For myself I find that break with value would be very useful as well as descriptive since I find that explicit control flow is cleaner and easier to read then using flag variables.


Some ideas (not necessarily fully thought out):

  • Break only with break Some(expr): this makes the type more explicit but would be annoying to type
  • Keep the implicit wrapping of values but define break () as break: this keeps the break/return consistency but at the cost of an inconsistency with break expr. It also means that any person who doesn't care about the type but only the Some(_) / None would need to define their own empty type

This is already the case:

let () = loop { break };

I should have been more clear. Sorry.

I meant that a possible solution could be break expr results in a for/while loop resulting in type Option<T> if expr === T. But break () does not change the behaviour but keep the for/while loop resulting type as () (not Option<()>)

This would require a type-dependent desugarring, which is extremely undesirable. It would be best if the desugaring acted almost like a macro.

What if break expr, but expr: ()

1 Like

I'm not sure where I stand on the feature overall, but I think I would prefer just requiring break Some(val); over magically producing options, which also helps with orthogonality with loop.

1 Like

So break in a for loop (and possibly while loop) always takes a Option or ()? That sounds more reasonable. I would like generators to be tightly integrated into our loop constructs, but that may not be important right now.

It will still require type-dependent desugaring because break value is ambiguous. But if we say, break is like finishing the loop normally and break Some(value) (where Some($expr) is part of the syntax) that would resolve the ambiguity (at the high cost of making it less orthogonal)

Unfortunately, any solution that makes the option constructor part of the syntax is also not really good. Then redefining Some or using a different path (e.g. std::option::Option::Some`) won't work.

I think the best and most consistent solution is to go towards "for over Generator", personally, though that's obviously long term.

2 Likes

I agree, I think that shifting over to generators is the best overall decision. Even better because generators are the next step up from Iterator

IIUC, you're basically thinking that the for structure should be changed so that it desugars to:

// this thing
for n in something { do_stuff(); break VALUE; }
// desugars to this
loop {
    match Generator::next(&mut something) {
        GeneratorResult::Yield(n) => { do_stuff(); break VALUE; }
        GeneratorResult::Return(v) => { break v; }
    }
}

You still won't be able to (usefully) employ break-with-value on an Iterator-based loop, because an Iterator<Item=T> would be a Generator<Item=T, Return=()>, so you would only be allowed to break ().

But at least it makes sense, and it would be useful in combination with generators and an iterator-to-generator combinator...

Well, you could with a map_return combinator. You could then set the return value of the generator to be whatever you want, and use break with whatever type of value you want in the loop.