Add `if x in option` syntax sugar

Currently we use if let Some(x) = option, which works, but is a bit verbose.
This will be more ergonomic, because we don’t need pattern matching in most of the times.

The same idea already applied in for x in iter syntax, which effectively replaces while let Some(x) = iter.next().
With implementing this, Rust will be more consistent.

Also, other std::ops::Try types might be supported.
Considering how commonly they are used, code should be easier to read, write, and maintain.

While I can see the analogy you’re making, I think if x in option comes across as ambiguous there. On the one hand, you could interpret it as matching against the first element, and on the other hand, you could interpret it as walking the whole iterator and comparing against each element.

9 Likes

For one, I’d find this confusing. I know python and it has if x a in c. However, x here is existing variable (or a constant) and it checks if it is present in the collection c. On the other hand, your code introduces x. I believe the if let syntax is more general and also suggestive ‒ I don’t see much difference in writing (it’s about the same amount of code), but I see the if let more readable. And it also doesn’t introduce more corner cases into the language.

8 Likes

Doesn’t for x in option do what you want?

6 Likes

for x in option don’t have else, and can’t return value other than () when used as expression

1 Like

So, to make this a little more concrete, I take it your intended desugaring would be something like:

if foo in bar {
    baz(foo)
} else {
    qux()
}
match Try::into_result(bar) {
    Ok(foo) => {
        baz(foo)
    }
    Err(_) => {
        qux()
    }
}

  1. Creating a new binding without a match or let is surprising.

  2. I would definitely find an if-in syntax like this confusing when most languages (that I know of) use this as some sort of operator-in.

  3. Using Try like this necessarily entails ignoring the error case, for an Option that’s ok, but if bar was a Result there’s nothing locally here telling you that you’re ignoring the error, compare with

    if let Ok(foo) = bar {
        baz(foo)
    } else {
        qux()
    }
    

    where the Ok(...) lets you know you’re dealing with a Result and ignoring the error case.

7 Likes

But isn't that how for x in iter works? The idea of this thread (which I'm unsure about overall) is that you can make a unary analog to for x in iter with if x in container, so it doesn't seem surprising to me.

3 Likes

There’s a problem with human ambiguity though; if x in container is handled as if container.contains(x).

For this reason I’m loosely in favor of not adding this, but could be convinced if someone could provide a benefit of not just writing it as if let Some(x) = container, which would probably be some application beyond just Option which has Some/None in the prelude and isn’t used as if let Option::Some(x) = container.

That's actually an interesting point. I wonder if Rust ever experimented with for let x in iter pre-1.0, that seems more consistent with most of the languages I can think of.

Pre-post edit: Browsing the rosetta code page on foreach it seems I just managed to come from the smaller subset of languages that require an explicit declaration in their foreach loops, most of them are consistent with Rust in that the foreach loop implicitly declares new bindings for the loop variable.

I agree that the most compelling reason not to do this I’ve seen in this thread is that other languages have x in y as a boolean operation.

2 Likes

But it's not surprising to create a new binding with in.

That's true, but I might argue that to those who don't know other languages and don't familiar with pattern matching, if x in option might be simpler than actual version. Also, it depends on context where you see this syntax, e.g. on the following examples it should be more straightforward:

    if next_value in iter.next() { ... }

    if value in container[key] { ... }

    if value in opt_value { ... }

That good point. So, this might be restricted only to Option<T> or something like std::ops::Container<T>

That's an interesting observation, but it's incomplete in a way that I think obscures a large part of the value: the IntoIterator::into_iter call and resulting temporary.

So what if there were an IntoOption trait? Would that be useful? I could imagine something like

if x in &some_mutex { x.foo() }

instead of

if let Ok(x) = some_mutex.lock() { x.foo() }

But I can't decide if lock() or try_lock() is right there, and I don't know if this is even ever a good idea.

(Especially if a option is Some(x) expression were to happen, like as been discussed before, which IMHO is short enough, while being more general and composing better, though I'm still uncertain about the binding scope questions.)

I think that proposed behaviour of if x in smth { .. } originates from false idea of coherency with for item in items { .. }. As I see it, the main reasons why we have this form of for syntactic sugar is because it is “nice to read” as “for every item in items do stuff”. So if we’ll look at if x in smth { .. } from this perspective we can see that coherent behaviour will be the Pythonic one, i.e. sugar for if smth.into_iter().any(|v| x == v) { .. }, because, well, we read it as “if x in smth do stuff”, and any other behaviour will be a constant source of cognitive dissonance.

5 Likes

I’d rather have if x in y mean the same thing it does in python. People learning rust after python are sure to appreciate that, I think, and with python being one of the most beginner-friendly languages, there will be a lot of people familiar with it.

Counterproposal:

Allowing else on for constructs would have the desired effect for usage with Option and would be useful in other situations, since empty collections is a common corner case. I don’t know if this was discussed in the past, but i assume so since this is a fairly obvious idea…

4 Likes

for else where the else-case is executed when the loop is never entered has been discussed before. Iirc the main reason we can’t use the the else keyword for that is because Python has poisoned the else syntax with a very different, unfitting meaning. It’s entered if the loop was not broken out of.

If there were an IntoOption or similar trait, it will be better to implement it for LockResult only, which will be more explicit:

if x in some_mutex.try_lock() { x.foo() }

if x in some_mutex.lock() 

I think that if option is Some(x) { ... } dont differs too much from if let Some(x) = option.
What will be wrong with x in option syntax to be also evaluated to bool?

What's more important is that this syntax reduces cognitive load when dealing with such code. It strips information that's obvious. Why should we read everytime that we deal with Option if we know that probably in 99% of the times?

But what's wrong with such interpretation? It's right and it even don't differs too much with Python version (in both versions we do checking if x is present in container, in both this might be true, and in both versions then we can "do stuff" on that x. Only one difference is where x introduced)

But for value in option is confusing and misleading and implementing else will not help with that.
Even when it reads fluently, when I see for I always expect loop to be next, and not sole value presence check.

What's worse is that such syntax is side-effect of implementing IntoIter for Option (which probably was primarily to be able to use Option in combinators like flat_map).

I hope in future it will be linted and if value in option will be proposed instead

I find that it can be useful to think about Option as a single-or-no-element iterator in other contexts as well. I think even outside of the utility of using it in contexts that take IntoIterator, it definitely should be IntoIterator.

for element in option { do stuff } is still correct, it’s just that the loop only runs zero or one times, rather than zero or more times. It’s calling regex ? a repetition ({0,1}) in addition to + ({1,}) and * ({0,}).

If a syntactical form existed for IntoOption to specifically cater to the zero-or-one case, then I’d support a clippy lint to prefer that when both IntoIterator and IntoOption are available, but not a rustc lint. I think they already get optimized to the same end code anyway, it’s just a matter of rustc being able to assume the block is run a maximum of one time for type and borrow checking.

The problem I see with if value in option is that I expect that to mean if option == Some(value) (or if option.contains(value), not if let Some(value) = option. for always creates a name, if never does, let does irrefutable pattern matching assignment, and if let does conditional scoped refutable pattern matching assignment.

At a glance, what do you expect the following code to do?

let option = Some(10);
let value = 5;

if value in option {
    println!("if  : value = {}", value);
} else {
    println!("else: value = {}", value);
}

for value in option {
    println!("for : value = {}", value);
}

My intuition says else: value = 5/for : value = 10, because so far I’ve been trained that if cannot introduce new bindings. That’s also why I’m weakly against if option is Some(value) without getting some other benefit from it (which I think exists but I cannot recall at the moment).

5 Likes