Add `if x in option` syntax sugar

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

10 Likes

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

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.

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.

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

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.

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()
	}, // `}`
    }

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.