Add `if x in option` syntax sugar


#21

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).


#22

I don’t think, for one, that if let Some(x) is such a burden or in fact that it’s hard to read or maintain at all. I don’t see the point in adding syntactic sugar for it at all. It’s neither long or verbose, nor does it need much repetition (it does exactly one thing).

I do find, however, that if x in something is pretty confusing because many languages (and programmers) interpret it as something.contains(x).


#23

Thinking about your reply I’ve found that IntoIterator, value in container and expression is pattern things might be conceptually arranged wery well and it will be not so hard to adapt to them.
Consider the following set of features:

  1. expression is pattern evaluates to bool and there no scoping issues
  2. val in container also evaluates to bool and there also no scoping issues
  3. IntoIterator provides in syntax and there no additional types like IntoOption
  4. for x in iterator is deprecated in favor of while x in iterator
  5. #[iter_once] used on Option<T> to promote using if instead of while and if is version
  6. #[iter_never] used on Result<T, E> to prohibit error case ignoring

#24

It uses too many unnecessary symbols to express that, and syntax is not very welcome.
We can live with that - surely, but I feel that feature which is so commonly used (not only in Rust) should have better syntax

I have strong feeling that if x in something syntax like in other languages is one of those things which are inconsistent and should not be copied blindly to please everyone (like classes, inheritance, manual memory management, switch statements, etc.).
If there will be benefits of feature I don’t see behavior of mainstream languages to be a significant problem when implementing it. Rust is different language and it can behave differently, it can implement things differently and even it positiones itself as language which does that.


#25

and there are no scoping issues

That’s terribly unspecific…

let iter: &[u32] = &[0, 5, 10][..];
let x = 5;
if x in iter {
    // does this work? What's the scoping?
    // there's "no scoping issues"
}

There’s nothing that evaluates to bool that also introduces a binding currently in Rust. And having a side effect is definitely a foot gun.


#26

Scoping is exact the same as with:

let iter: &[u32] = &[0, 5, 10][..];
let x = 5;
for x in iter {
    // does this work? What's the scoping?
    // there's "no scoping issues"
}

x in iter part certainly says that new binding is introduced. Personally I’ve never had any problem with code like that.

I saw some proposals to evaluate x is Some(y) expressions to bool. All problems with scoping and visibility was around || operator.
All that I’ve written in previous comment depend on that


#27

That’s where we differ I guess. For me, for is the construct that does the introducing of the binding for me. I read a for, new binding. I read an if, it’s an expression, and thus doesn’t introduce a new binding.

At least currently, $ident in $expr is not an expression. The construct is for ... in ... { ... }, and in is not its own thing; for is the actual driving force in this situation.

Let’s be explicit: what is your suggested desugar for each of: (and consequently, which pass typeck)

Given a pre-existing variable it, statement context

x in it;
x in it;
x in it;
iter::once(x in it);
let y = x in it;
let y = x in it;
let z = x in it;
if x in it {
    do_something(x);
}
let x = if x in it {
    do_something(x)
} else {
    default()
};
if x in it {
    do_something(x);
}
if x in it {
    do_something(x);
}
if x in iter::once(x in it) {
    do_something(x);
}
let y = iter::once(x in it);
if x in y {
    do_something(x);
}

(Repeat every case of if ... in ... { with for and while instead of if)

Illegal construct is a viable answer. Please make sure that you provide a reasoning for what is and isn’t legal, though, and it cannot depend on types – that’s typeck, which is after the parse.

EDIT 1: since you seem to be implying that x in iter would be an expression, throw in some other combinations like logical operators as well, x1 in it1 && x2 in it2, x in it1 || x in it2, in the above contexts as well.

EDIT 2: if you can provide a solid consistent rule for how in is desugared that can be applied to all of these cases, you don’t have to enumerate them; I/we can pick out the problematic ones then.


#28

Probably we have that difference because I don’t rely on intuition when reading code. I don’t associate “human” and “programming” language, so I see for x in y without implied “each”, and if x in y without “some/this”, therefore they only differs in “if/for” prefix for me.

I’m sure that my examples are invalid, but at least they should demonstrate the idea:

x in it;
⇒
let __i_1: Iterator<Item=i32> = IntoIterator::into_iter(it);
match __i_1.next() {
    Some(x) => {  // WARNING: unused variable `x`
        true 
    },
    None => { 
        false 
    },
};

Here __i_1 is autogenerated unique name. It also should be invisible further, but I dont know how to express that


let y = x in it;
let z = x in it;
⇒
let __i_1: Iterator<Item=i32> = IntoIterator::into_iter(it);
let y = match __i_1.next() {
    Some(x) => {  // WARNING: unused variable `x`
        true
    },
    None => { 
        false
    },
};
let __i_1: Iterator<Item=i32> = IntoIterator::into_iter(it); // ERROR: use of moved value `it`
let z = match __i_2.next() { 
    Some(x) => { // WARNING: unused variable `x`
        true 
    },
    None => {
        false
    },
};

This just demonstrates how moving and borrowing should work.


iter::once(x in it);
⇒
let __i_1: Iterator<Item=i32> = IntoIterator::into_iter(it);
iter::once(match __i_1.next() {
    Some(x) => {  // WARNING: unused variable `x`
        true
    },
    None => { 
        false
    },
});

Desugaring in if context will be different:

let x = if x in it {
    do_something(x)
} else {
    default()
};
⇒
let __i_1: Iterator<Item=i32> = IntoIterator::into_iter(it);
let x = match __i_1.next() {
    Some(x) => { 
        do_something(x) 
    },
    None => { 
        default() 
    },
};

It will be context dependent, e.g. if x in it is desugared differently than x in it


Since for and while will be the same (loop until condition is true), there will be only one example that uses while:

while x in it {
    do_something(x)
}
⇒
let __i_1 = IntoIterator::into_iter(it);
loop {
    match __i_1.next() {
        Some(x) => { 
            do_something(x) 
        },
        None => { 
        },
    }
}

What I wanted to say is that syntax proposed by me is similar to <expr> is <pat> that was proposed and partially implemented by @petrochenkov, as alternative to if let with multiple patterns.

Expressions like x in it will be similar to iterator_on_it.next() is Some(x), so there might be common parts, like integration with && and ||.

I guess, desugaring might rely on turning && operator placed near x in it expressions into nested if/match expressions (and probably that will be more than syntax sugar, because we need to flatten all generated else branches into one). Desugaring with || operator is another challenge, because that syntax by itself might introduce indeterminism in cases like (x in it1 || true) && x.use(), and that should be prohibited (here’s my attempt to describe how); additionally all generated if→then branches should be flatten into one.

Some naive examples:

if x in it && x.cond() {
    do_something(x);
}
⇒
let __i_1 = IntoIterator::into_iter(it);
match __i_1.next() {
    Some(x) => { 
        if x.cond() {
            do_something(x);
        } else {
        }
    },
    None => {
    }
}

let y = if cond() && x in it {
    do_something(x)
} else {
    default()
};
⇒
let __i_1 = IntoIterator::into_iter(it);
if cond() {
    match __i_1.next() {
        Some(x) => { 
            do_something(x)
        }, 
        None => {
           // FLATTEN with `else` below
        }
    }
} else {
    default()
};

let y = if x in it1 || x in it2 {
    do_something(x)
} else {
    default()
};
⇒
let __i_1 = IntoIterator::into_iter(it1);
let __i_2 = IntoIterator::into_iter(it2);
let y = match __i_1.next() {
    Some(x) => { 
        do_something(x)
    },
    None => {
        match __i_2.next() {
            Some(x) => { 
                // FLATTEN with `then` above
            },
            None => {
                default()
            }
        }
    }
}

Below is complex example that demonstarates idea how to flatten branches using Option type:

let y = if (x in it1 && x.cond() || cond() && x in it2) && x.cond2() {
    do_something(x)
} else {
    default()
};
⇒
let __i_1 = IntoIterator::into_iter(it1);
let __i_2 = IntoIterator::into_iter(it2);
let y = {
    let x = match __i_1.next() { // `(x in it1`
	Some(x) => { 
	    if x.cond() {  // `&& x.cond()` 
		Some(x)
	    } else {
		None
	    }
	},
	None => {
	    None
	},
    };
    let x = match x { // `||`
	Some(x) => {
	    Some(x)
	},
	None => {
	    if cond() { // `cond() &&`
		match __i_2.next() { // `x in it2)`
		    Some(x) => {
			Some(x)
		    },
		    None => {
			None
		    },
		}
	    } else {
		None
	    }
	},
    };
    let y = match x { // `&&`
	Some(x) => {
	    if x.cond2() { // `x.cond2()
		Some({ // `{`
		    do_something(x)
		}) // `}`
	    } else {
		None	
	    }
	},
	None => {
	    None
	},
    };
    match y {
	Some(y) => {
	    y
	},
	None => { // `else {`
	    default()
	}, // `}`
    }