Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow negation of if let #2616

Open
mqudsi opened this issue Dec 22, 2018 · 87 comments
Open

Allow negation of if let #2616

mqudsi opened this issue Dec 22, 2018 · 87 comments
Labels
A-control-flow Proposals relating to control flow. A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@mqudsi
Copy link

mqudsi commented Dec 22, 2018

The RFC for if let was accepted with the rationale that it lowers the boilerplate and improves ergonomics over the equivalent match statement in certain cases, and I wholeheartedly agree.

However, some cases still remain exceedingly awkward; an example of which is attempting to match the "class" of a record enum variant, e.g. given the following enum:

enum Foo {
    Bar(u32),
    Baz(u32),
    Qux
}

and an instance foo: Foo, with behavior predicated on foo not being Bar and a goal of minimizing nesting/brace creep (e.g. for purposes of an early return), the only choice is to type out something like this:

    // If we are not explicitly using Bar, just return now
    if let Foo::Bar(_) = self.integrity_policy {
    } else {
        return Ok(());
    }

    // flow resumes here

or the equally awkward empty match block:

    // If we are not explicitly using Bar, just return now
    match self.integrity_policy {
        Foo::Bar(_) => return Ok(());
        _ => {}
    }

    // flow resumes here

It would be great if this were allowed:

    // If we are not explicitly using Bar, just return now
    if !let Foo::Bar(_) = self.integrity_policy {
        return Ok(());
    }

    // flow resumes here

or perhaps a variation on that with slightly less accurate mathematical connotation but far clearer in its intent (you can't miss the ! this time):

    // If we are not explicitly using Bar, just return now
    if let Foo::Bar(_) != self.integrity_policy {
        return Ok(());
    }

    // flow resumes here

(although perhaps it is a better idea to tackle this from an entirely different perspective with the goal of greatly increasing overall ergonomics with some form of is operator, e.g. if self.integrity_policy is Foo::Bar ..., but that is certainly a much more contentious proposition.)

@Centril Centril added T-lang Relevant to the language team, which will review and decide on the RFC. A-syntax Syntax related proposals & ideas A-expressions Term language related proposals & ideas A-control-flow Proposals relating to control flow. labels Dec 22, 2018
@H2CO3
Copy link

H2CO3 commented Dec 23, 2018

or the equally awkward empty match block

I don't understand what's "awkward" in a block.

Furthermore, I don't see any good syntax for realizing this. Certainly, both proposed ones (if !let foo = … and if let foo != …) are much more awkward and a lot harder to read than a perfectly clear match. I would consider such code less ergonomic and less readable than the equivalent match expression.

@burdges
Copy link

burdges commented Dec 23, 2018

We've seen numerous proposals for this, so maybe review them before attempting to push any serious discussion.

If the variants contain data then you normally require said data, but you cannot bind with a non-match, so is_[property] methods are frequently best at covering the actual station. If is_[property] methods do not provide a nice abstraction, then maybe all those empty match arms actually serve some purpose, but if not then you can always use a wild card:

let x = match self.integrity_policy {
    Foo::Bar(x) => x,
    _ => return Ok(()),
}

Also, one can always do very fancy comparisons like:

use std::mem::discriminant;
let d = discriminant(self.integrity_policy);
while d != discriminant(self.integrity_policy) {  // loop until our state machine changes state
    ...
}

I believe the only "missing" syntax around bindings is tightly related to refinement types, which require a major type system extension. In my opinion, we should first understand more what design by contract and formal verification might look like in Rust because formal verification is currently the major use case for refinement types. Assuming no problems, there is a lovely syntax in which some refine keyword works vaguely like a partial match. As an example, let y = x? would be equivalent to

let Ok(y) = refine x { Err(z) => return z.into(); }  

In this let Ok(y) = .. could type checks because refine returns a refinement type, specifically an enum variant type ala #2593. I suppose refinement is actually kinda the opposite of what you suggest here, but figured I'd mention it.

@mqudsi
Copy link
Author

mqudsi commented Dec 23, 2018

I looked around for past proposals regarding expanding the if let syntax, starting with the original if let rfc and issues referenced in the comments before opening this.

@burdges Thanks for sharing that info, I have some reading to do. But with regards to

if is_[property] methods do not provide a nice abstraction, then maybe all those empty match arms actually serve some purpose

No, those are exactly what I would like, except they're not available for enum variants, i.e. for enum Foo { Bar(i8), Baz } there is no (automatically-created) Foo::is_bar(&self) method. It is in fact the absence of such an interface that required the use of if let or match.

One problem with the existing if let syntax is that it is a wholly unique expression masquerading as an if statement. It isn't natural to have an if statement you can't negate - the default expectation is that this is a boolean expression that may be treated the same as any other predicate in an if clause.

@H2CO3
Copy link

H2CO3 commented Dec 23, 2018

No, those are exactly what I would like, except they're not available for enum variants.

Oh, I think that can be fixed quite easily. They could be #[derive]d easily based on naming conventions. I'll try to write up an example implementation soon.

@burdges
Copy link

burdges commented Dec 23, 2018

I'd suggest #[allow(non_snake_case)] for autogenerated one, so they look like

    #[allow(non_snake_case)]
    pub fn is_A(&self) { if let MyEnum::A = self { true } else { false } }

That said, if you have more than a couple variants then is_ methods might describe meaningful variants collections, not individual variants. We're talking about situations where you do not care about the variant's data after all.

In fact, you'd often want the or/| pattern like

let x = match self.integrity_policy {
    Foo::A(x) | Foo::B(x,_) => x,
    Foo::C { .. } | Foo::D => return Ok(()),
    Foo::E(_) => panic!(),
}

@H2CO3
Copy link

H2CO3 commented Dec 24, 2018

I made a PoC implementation of the "generate is_XXX methods automatically" approach.

@Centril
Copy link
Contributor

Centril commented Dec 24, 2018

@H2CO3 This already exists on crates.io: https://crates.io/crates/derive_is_enum_variant and there's likely more complex variants too involving prisms/lenses and whatnot.

It seems to me however that generating a bunch of .is_$variant() methods is not a solution that scales well and that the need for these are due to the lack of a bool typed operation such as expr is pat (which could be let pat = expr... -- color of bikeshed...).

Fortunately, given or-patterns (rust-lang/rust#54883) let chains (rust-lang/rust#53667), and a trivial macro defined like so:

macro_rules! is { ($(x:tt)+) => { if $(x)+ { true } else { false } } }

we can write:

is!(let Some(E::Bar(x)) && x.some_condition() && let Other::Stuff = foo(x));

and the simpler version of this is:

is!(let E::Bar(..) = something)
// instead of: something.is_bar()

@H2CO3
Copy link

H2CO3 commented Dec 24, 2018

@Centril I'm glad it already exists in a production-ready version. Then OP can just use it without needing to wait for an implementation.

there's likely more complex variants too involving prisms/lenses and whatnot.

What do you exactly mean by this? AFAIK there aren't many kinds of enum variants in Rust. There are only unit, tuple, and struct variants. I'm unaware of special prism and/or lens support in Rust that would lead to the existence of other kinds of variants.

It seems to me however that generating a bunch of .is_$variant() methods is not a solution that scales well

I beg to differ. Since they are generated automatically, there's not much the user has to do… moreover the generated functions are trivial, there are O(number of variants) of them, and they can be annotated with #[inline] so that the binary size won't be bigger compared to manually-written matching or negated if-let.

Fortunately, given or-patterns [and] let chains

Those are very nice, and seem fairly powerful. I would be glad if we could indeed reuse these two new mechanisms plus macros in order to avoid growing the language even more.

@Centril
Copy link
Contributor

Centril commented Dec 24, 2018

@H2CO3

@Centril I'm glad it already exists in a production-ready version. Then OP can just use it without needing to wait for an implementation.

🎉

What do you exactly mean by this? AFAIK there aren't many kinds of enum variants in Rust. There are only unit, tuple, and struct variants. I'm unaware of special prism and/or lens support in Rust that would lead to the existence of other kinds of variants.

I found this a while back: https://docs.rs/refraction/0.1.2/refraction/ and it seemed like an interesting experiment; other solutions might be to generate .extract_Bar(): Option<TupleOfBarsFieldTypes> and then use ? + try { .. } + .and_then() pervasively. You can use .is_some() on that to get the answer to "was it of the expected variant".

I beg to differ. Since they are generated automatically, there's not much the user has to do… moreover the generated functions are trivial, there are O(number of variants) of them, and they can be annotated with #[inline] so that the binary size won't be bigger compared to manually-written matching or negated if-let.

In a small crate I don't think it would pay off to use a derive macro like this; just compiling syn and quote seems too costly to get anyone to accept it. Another problem with the deriving strategy is that when you are working on a codebase (say rustc) and you want to check whether a value is of a certain variant, then you first need to go to the type's definition and add the derive macro; that can inhibit flow.

Those are very nice, and seem fairly powerful. I would be glad if we could indeed reuse these two new mechanisms plus macros in order to avoid growing the language even more.

Sure; this is indeed nice; but imo, it seems natural and useful to think of let pat = expr as an expression typed at bool which is true iff pat matches expr and false otherwise. In that case, if !let pat = expr { ... } is merely the composition of if !expr { ... } and let pat = expr.

@alexreg
Copy link

alexreg commented Dec 29, 2018

I'm for this. It arguably reduces the semantic complexity of the language, and improves consistency, especially with let-chaining coming in.

Has @rust-lang/libs thought of bundling the is! macro with the language, given how common it is?

@mbrubeck
Copy link
Contributor

mbrubeck commented Jan 8, 2019

See also #1303.

@graydon
Copy link

graydon commented Jan 12, 2019

Opposed. Not a big enough use-case to warrant extension at language level. Similar to there not being both while and do while loops, it can be done via numerous user-level escape hatches provided above.

@mqudsi
Copy link
Author

mqudsi commented Jan 12, 2019

It is respectfully nothing like the while vs do while situation. A while loop is one thing, and do while is another. An if statement already exists in the language with clearly defined semantics and permutations. This syntax reuses the if statement in name only, masquerading as a block of code while not actually sharing any of its features apart from reusing the same if keyword, with no reason why that can’t be fixed.

@graydon
Copy link

graydon commented Jan 12, 2019

I regard if let the same way you do: as a random recycling of if somewhat out of context; I wouldn't have voted for its addition. But disliking it doesn't mean I think it needs to get more ways to mean something else yet again. It does not. If you dislike my analogy, you can substitute the analogy that we don't (and should not gain) a match-not expression.

(And definitely also not a guard-let or whatever Swift has. It has too many of these.)

@comex
Copy link

comex commented Jan 13, 2019

Which is why I think let should become an actual boolean expression, albeit with special rules around scoping. That solves the “random recycling” problem and also enables if !let.

@comex

This comment has been minimized.

@misha-krainik
Copy link

In Ruby exists unless operator where executes code if conditional is false. If the conditional is true, code specified in the else clause is executed.
It may be like

unless let Foo::Bar(_) = self.integrity_policy {
        return Ok(());
}

@mark-i-m
Copy link
Member

I’ve always disliked unless operators in languages that have them. There is some cognitive overhead for me to deal with the extra implicit negation.

@vext01
Copy link

vext01 commented Feb 5, 2019

Hi,

I've landed here looking for if !let. I wanted to write:

if !let Ok((Index{num_mirs: 0}, None)) = Decoder::new(&mut curs) {
    panic!();
}

Which I think today, is best expressed as:

match Decoder::new(&mut curs) {                                                                 
    Ok((Index{num_mirs: 0}, None)) => (),                                     
    _ => panic!(),                                                            
}

@scottmcm
Copy link
Member

@mark-i-m I dislike them when they're just sugar for !, but if there's a difference in what they do (like the unless block must be : !), then it might be plausible. The negation, given the !-typed block, is consistent with things like assert!, so I don't think it's fundamentally confusing.

@mark-i-m
Copy link
Member

I feel like that would be a bit unexpected for most people. There is conceptually no reason unless foo {...} would not be a normal expression like if or match.

@Pzixel
Copy link

Pzixel commented May 6, 2019

See #1303

@nox
Copy link
Contributor

nox commented Jul 25, 2019

I feel like that would be a bit unexpected for most people. There is conceptually no reason unless foo {...} would not be a normal expression like if or match.

AFAIK nobody is confused about guard statements in Swift.

@HeroicKatora
Copy link

HeroicKatora commented Sep 29, 2019

The usage of a pattern in if !let Foo::Bar(_) = self.integrity_policy is extremely confusing to me. What if I don't ignore the content but use a name pattern? It wouldn't be visible in the block the follows as that is executed when the destructuring failed but the if also makes this a single pattern.

if !let Foo::Bar(policy) = self.integrity_policy else {
    // Wait, `policy` is not a name here.
    return Ok(policy);
}

Guard blocks as in #1303 don't have this problem.

@RustyYato
Copy link

@HeroicKatora I think you linked to the wrong RFC (I don't think you mean to link to supporting comments in rustdoc)

@teohhanhui
Copy link

You can rewrite every if let with match

No, if let supports bindings.

But I agree that the ergonomics are not great with !matches

@Pzixel
Copy link

Pzixel commented Aug 4, 2020

Could you provide any example of if let that cannot be rewritten as match?

@teohhanhui
Copy link

if let Some(b) = buf_b.as_ref() {
    let b = b.clone();
    drop(buf_b);
    observer(ctx, &func.call_mut(ctx, &(a.clone(), b)));
}

here b is being bound. Of course you can assign it to a variable manually in the body of the if block, but we can see that if let allows more terse code.

@Pzixel
Copy link

Pzixel commented Aug 4, 2020

how is that different of

match buf_b.as_ref() {
  Some(b) => {
    let b = b.clone();
    drop(buf_b);
    observer(ctx, &func.call_mut(ctx, &(a.clone(), b)));
  },
  _ => {}
}

? Especially if we add an else clause. Of course it's terser but just a bit.

@teohhanhui
Copy link

@Pzixel If you read the original RFC for if let it's designed to solve exactly this case. Removing boilerplate code.

@Pzixel
Copy link

Pzixel commented Aug 5, 2020

I agree that if let is terser. I just said it completely support all features if let has like binding or anything else.

@SOF3
Copy link

SOF3 commented Aug 6, 2020

You can rewrite every if let with match

No, if let supports bindings.

But I agree that the ergonomics are not great with !matches

Do you mean !matches!()? Can you explain what the problem is, other than the potentially confusing ! operator (which applies to negation of all Boolean expressions in general)?

@alercah
Copy link
Contributor

alercah commented Nov 12, 2020

The critical thing that almost any approach using match lacks is the ability to extend a binding into the outer scope, and therefore avoid indentation.

If you're performing validation on some type that is four nested layers deep, for instance, with if left it ends up looking like this:

if let Some(a) = foo() {
  if let Some(b) = a.b {
    if let Some(c) = b.c {
      return Ok(c);
    } else {
      return Err("no c");
    }
  } else {
    return Err("no b");
  }
} else {
  return Err("no a");
}

This requires both a reader and writer of the code to keep track of the various levels of nesting and what condition was checked at each stage. It is quantifiable more difficult to work with code that has deep nested logic like this; it requires keeping more state in your head as you read and it will slow development down.

With enough layers of nesting (and anyone who doesn't believe that they can get this deep should look at rustc source), you make more readable code by ignoring the ergonomic convenience of pattern destructuring:

let f = foo();
if f.is_none() {
  return Err("no a");
}
let a = f.unwrap();
if a.b.is_none() {
  return Err("no b");
}
let c = a.b.unwrap();
// etc.

This code is far easier to work with. But better still would be:

unless let Some(a) = foo() {
  return Err("no a");
}
unless let Some(b) = a.b {
  return Err("no b");
}
// etc.

I used unless just because I think it's more readable than if !let, but I don't really care what colour the shed is.

However, I realized today, and the reason I'm poking in, is that I think it's maybe doable if you allow the use of the never type as a way to statically assert that a block can never return control flow normally.

Specifically, a macro where unless_let!((Some(a), Some(b)) = foo; { return; }) is expanded out to something like this: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=a9a29e6449207f569d52c1a43f9275fc

Unfortunately it requires a proc macro to get the extra names to bind to, but otherwise I think it could work pretty well.

@kennytm
Copy link
Member

kennytm commented Nov 12, 2020

However, I realized today, and the reason I'm poking in, is that I think it's maybe doable if you allow the use of the never type as a way to statically assert that a block can never return control flow normally.

I'm not sure if we should force "never return". We could support, for instance

let Some(a) = foo() else {
    a = 1234;
}
// meaning: let a = foo().unwrap_or(1234);
use_(a);

If we don't diverge or assign a inside the else branch, all future usage will just cause the "use of uninitialized variable" error, so no additional check is necessary.

@kennytm
Copy link
Member

kennytm commented Nov 12, 2020

Unfortunately it requires a proc macro to get the extra names to bind to, but otherwise I think it could work pretty well.

The biggest problem is how do you determine whether an identifier is a name or a constant.

#[cfg(wut)]
const B: u64 = 1u64;

let (Some(A), B) = foo() else { return };
dbg!(A, B);

@alercah
Copy link
Contributor

alercah commented Nov 12, 2020 via email

@kennytm
Copy link
Member

kennytm commented Nov 13, 2020

The macro could accept a metatoken to
denote a constant, perhaps, to allow the user to disambiguate.

Well we do have const in pattern, but that means we need to add it on everything variable-like even including None 🤔

let (Some(a), const { None }) = ...

@alercah
Copy link
Contributor

alercah commented Nov 15, 2020

We have const in patterns? I can't find any reference to it anywhere...

I implemented this over the weekend as a proof-of-concept, requiring the use of a@_ to disambiguate to the macro's parser that you are doing a binding and not a const. Not great, but it works. I'll look into posting it soon.

Meanwhile, an alternative syntactic suggestion from @strega-nil, that I quite like:

let Some(a) = opt else {
  return err;
}

I like this one in that it works naturally with chaining, too:

let Some(a) = opt &&
    Some(b) = a.field else {
  return err;
}

@CryZe
Copy link

CryZe commented Nov 15, 2020

@kennytm
Copy link
Member

kennytm commented Aug 23, 2021

can we close this now, given #3137?

@jhpratt
Copy link
Member

jhpratt commented Aug 23, 2021

That seems reasonable. The use case likely still exists, but is drastically reduced at this point.

@MrMino
Copy link

MrMino commented Jan 22, 2022

The usecases outlined here get even worse with while lets. Do they have any #3137 counterpart?

@goose121
Copy link

goose121 commented Feb 28, 2022

The usecases outlined here get even worse with while lets. Do they have any #3137 counterpart?

You can trivially implement them as a macro by example using let..else (although the syntax I was able to get macro_rules to accept was a bit ugly)

@MrMino
Copy link

MrMino commented Mar 5, 2022

@goose121 Yes, this would work, but this is anything but "trivial". I'd rather rewrite the whole loop and the code around it. Using a macro makes such code a lot less readable and doesn't benefit from language orthogonality.

If I have a feature that negates if let then I can apply the same intuition to while let and don't have to spend brain cycles on it too much. If I need to use a macro, I need to know that particular implementation of that macro, remember how to use it, etc.

@markusdd
Copy link

markusdd commented Jan 27, 2023

came here, suprised this does not work^^

I actually need while ! let in my case, but it boils down to the same thing.
In my usecase I am polling a process that I launched. If poll does not return the exitcode yet, I capture the output and forward it to my gui, if it is finished I want to move on.

@SimonSapin
Copy link
Contributor

Another option is while !matches!(expression, pattern)

@SOF3
Copy link

SOF3 commented Jan 28, 2023

Isn't it generally considered a bad code style to negate the condition of if if an else exists?

@markusdd
Copy link

Isn't it generally considered a bad code style to negate the condition of if if an else exists?

I would say this really depends on the use case. I e.g. did not want a plain if but a condition in a while loop.

My use-case is polling a popen-object until it returns a Some() (indicating the process has terminated).

@c-git
Copy link

c-git commented Jan 28, 2023

Isn't it generally considered a bad code style to negate the condition of if if an else exists?

I would say this really depends on the use case. I e.g. did not want a plain if but a condition in a while loop.

My use-case is polling a popen-object until it returns a Some() (indicating the process has terminated).

Is the object being polled an Option?

@markusdd
Copy link

markusdd commented Jan 28, 2023

yup, subprocess crate:
grafik

I am basically polling if the process runs, if yes I keep collecting the output and send it to my gui, if has finished I evaluate the exit code (so when I get a Some() ) and move on.

@c-git
Copy link

c-git commented Jan 28, 2023

I tried to play around with your use case a bit and I ran into a few "challenges". My code and what I tried is below. The challenge that I had was that if I used a while loop I couldn't find a way to get the ExitStatus. However if the negation were allowed on the let how would you access the value (wouldn't it not be available after the while block where you would need to use it)?

Here is the code I used to play with the idea (let me know if I misunderstood).

playground link

fn poll(step:u8) -> Option<u8>{
    if step > 5{
        Some(0)
    } else {
        None
    }
}

fn main() {
    let mut step = 1;
    loop{
        if let Some(exit_code) = poll(step){
            println!("Exiting with exit code: {exit_code}");
            break;
        }
        println!("Step: {step}");
        step += 1;
    }
    println!("Done");
}

First off I can see why you would want to put it in a while loop but I also couldn't find a way to do it, that allowed me to still find out what the exit code was.

If you didn't need the exit code you could just do the following:

playground link

fn poll(step:u8) -> Option<u8>{
    if step > 5{
        Some(0)
    } else {
        None
    }
}

fn main() {
    let mut step = 1;
    while poll(step).is_none(){
        println!("Step: {step}");
        step += 1;
    }
    println!("Done");
}

@markusdd
Copy link

markusdd commented Jan 28, 2023

I eventually need the code.
So what I do now is I just loop {} and then match and break.

This is more cumbersome than it needs to be but it works:
grafik

@tbu-
Copy link
Contributor

tbu- commented Jan 27, 2024

We kinda have this as a combination of let … else and !matches!(…) now. let … else supports bindings and !matches!(…) works for cases without bindings.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-control-flow Proposals relating to control flow. A-expressions Term language related proposals & ideas A-syntax Syntax related proposals & ideas T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests