Idea: Early returns and irrefutability

I do like this idea, but I'm a little bit worried. Doesn't the type of stream change somewhat surprisingly if you exclude the || continue part? It becomes an Option instead:

let stream: Option<&mut Send> = self.streams.get_send_mut(id);



Would the following syntax be clearer?

let Some(stream) = self.streams.get_send_mut(id) || continue;

No more surprisingly than any other operator. C# has demonstrated that x ?? 0 (where x: int?) or x ?? throw or x ?? continue or whatever is completely understandable in practice.

Maybe the Elvis operator ?: as seen in Kotlin would make more sense here instead of overloading ||? I like that it's like an extension of the simple ? try operator (and it aligns with second-value-optional ternary operators in some other languages).

1 Like

Whether this overloads anything depends on whether it's encoded by an ad-hoc mechanism or whether it is just the combination of let p = e (as an expression typed at bool), ||, and continue.

In the latter case, the compiler would understand that p is refutable, and so bindings_of(p) might not be definitely initialized after let p = e. However, continue : ! holds so we know that if we reach beyond continue then bindings_of(p) is definitely initialized. (We're back to the flow based logic discussed previously...)

There's a parser issue here tho... let p = e || continue would need to be parsed as (let p = e) || continue.

I was expecting it'd desugar to the if I mentioned in the other thread -- it'd just remove || entirely the same way that ? gets removed entirely. (With a bool: Try it'd be fully compatible with existing code, too.)

While a syntax that requires a pattern on the left hand side, i.e.

let Some(v) =

is neutral to the type of enum to be matched and can bind any of its variants:

let Ok(v) = ... || ... ;
let Err(e) = ... || ... ;
// or
let Msg::A(m) = ... || ... ;
let Msg::B(m) = ... || ... ;
let Msg::C(m) = ... || ... ;

it makes sense to optimize and streamline for ops::Try, and specifically the success path. This is what would benefit the ergonomics the most, I think. The code that is written, and read, is concise and to the point while being explicit about any error variants.

The || operator is suitable because:

  1. It intuitively deals with truth values of sorts (success or failures of expectations).
  2. It short-circuits on the first confirming alternative, and only lazily evaluates the rhs.

Because the || already parses, I don’t know if this syntax is possible.

let <pat> = <expr> || <expr> ; already parses as let <pat> = <expr/binop (|| <expr> <expr>)> ;.

?? isn’t possible because that has meaning as ? ?. ?: has a planned meaning in ? : where the colon is for type ascription. Any new token changes decl macro behavior anyway.

I think the syntax has to use a keyword, unfortunately. (Though I’d love to be proven wrong!) <expr> ?? <expr> doesn’t have a parse currently, so I could be convinced. (<expr> ?: <expr> could definitely look like <expr> ? : <type>.)

1 Like

Actually, scratch that. Because || is a built-in we can do whatever the heck we want. I’m not sure how intuitive it’d be though.

Javascripty was wrong because it was late at night. Scott correctly corrected me to Perly; I was thinking of Javascript confounding short-circuit or and null coalescion (which is awful, they want to be separate operators).

You're preaching to the choir here, I noted that they type at ! in my post. But, the fact that it's an "obvious" consequence of the type system doesn't mean it isn't an abuse of what these things are meant for (you know, short-circuiting =P), and should be linted against. Rule of weird, etc.

I'm not disagreeing with this, I'm saying that doing non-local control flow with ||, as opposed to if/else or match, is going to make your reviewers upset.

Also, I'd like to note that I think that adding an Elvis operator and using ?: return is not better than if let {} else {return}. At that point, we might as well just add the C ternary because writing an if inline is too many braces...

1 Like

I’ve suggested this syntax before, but:

match let Ok(x) = res {
    Err(e) => something_diverging(e),
}

cc @comex re. this topic since I know they like let as a bool-typed expression (I'm generally in favor as well).

I think it's neither weird nor abuse. Tho the convention is to use if, if I read self.method() || continue; it would not be strange to me but rather readable.

This is subjective. I assume readers of my code are familiar with, and conform to, the Google C++ style guide; you’re from a different school of programming entirely. I don’t think this is at all worth bikeshedding.

1 Like

It was my impression so far that Rust doesn’t do coercion to bool (for example, if 0 { isn’t allowed), unlike for example in Python, and I think that fits with the general style of Rust. Similarly, || currently only appears to accept bool (that is, 1 || 3 is not allowed). It seems like a sizable departure from this style to start coercing Option or other Try types to bool.

I find this pretty hard to grok. In particular, the asymmetry between the first case being outside of the block and other case(s) being in the block makes it less than obvious to me.

3 Likes

This is no better than just doing

let x = match res {
    Ok(x) => x,
    Err(e) => something_diverging(e)
};

Which is already valid. The only benefit to yours over this is a few less characters, which isn’t a sizable benefit.

2 Likes

It does with unary/binary ambiguity, like x ?? - y

3 Likes

How about that:

fn func(x: Option<u8>) -> Option<u8> {
    let y = x? + 2;
    Some(y)
}

?

It would be more like

let x = match x {
    Some(x) => x,
    None => return expr
};

Where expr is not necessarily an Option<_> or Result<_>.

Hmm. I do like that this avoids the need for ‘magic’ scoping rules as would be required for if !let, and it’s even more succinct. On the other hand, it feels a bit weird to be bringing a Perlism to Rust.

The parsing issue is interesting. As has been noted, code of the form:

let Foo(x) = expr || expr2;

parses today as let Foo(x) = (expr || expr2). But I believe this can never type-check, as || always returns a bool, while a pattern of the form Foo(x) can never match a bool. (Handy that we don’t have ‘pattern aliases’, nor do we allow function calls in constant patterns.) So in theory, let could adjust its precedence based on the syntactic form of the pattern.The only patterns a bool can match are variables, constants, and literals. Variables are irrefutable, so not usable with the proposed new feature. Bool literals can be identified at parse time; bool-typed constants can’t, but neither literals nor constants are particularly useful for this feature since they don’t introduce a binding – so it’s ok if, e.g., let true = x || continue continues to parse the old way.

For let Foo(x) = expr || expr2 to parse as (let Foo(x) = expr) || expr, the let Foo(x) = part would have to have stronger precedence than ||. For consistency, you would also expect it to have stronger precedence than anything weaker than ||.

According to the reference, the operators with weaker precedence than || are .. and ..=, assignment operators (=, +=, etc.), return, break, and closures. Assignment operators return () and can’t be matched against Foo(x) either. return, break, and closures are prefix operators, so they can’t conflict with let which is effectively another prefix operator. That leaves .. and ..=, and they actually do pose a problem, as this compiles:

let std::ops::Range { start, end } = 1 .. 2;

And although std::ops::Range { start, end } is irrefutable, it cannot be distinguished at parse time from a refutable enum variant pattern. Thus, adding this feature might require breaking that code (at least, it would require significant additional complexity to not break it). On the other hand, building and then immediately destructuring a range is fairly useless and unlikely to be seen in real code. This could also be postponed until an edition boundary.

In any case, a potential alternative is to just require parentheses:

(let Foo(x) = bar) || continue;

I’d say this is uglier, but it arguably has readability benefits, as it’s more obvious that this is not a regular let, and the || continue sticks out more.

1 Like

To clarify, what I think Centril is proposing does not actually involve coercion to bool – unlike the Perl and JavaScript constructs which serve as semi-inspiration. Rather, the idea is that

let Foo(x) = my_enum || continue;

would parse as

(let Foo(x) = my_enum) || continue;

let would be a bool-typed expression, evaluating to true or false based on whether the pattern matched (sort of retroactively justifying if let). It wouldn't matter whether the value of my_enum itself was 'truthy' or 'falsy' or anything like that.

1 Like