Replace `if let` syntax

I tend to avoid if let because it doesn't feel readable/clear to me what it means. It sounds like it's a let statement, but in reality it's a shorthand for the match statement which is quite different. A match statement can fail, but let statements cannot. if let confuses those two concepts.

I have three suggestions for alternatives:

// 1
if match self.value => Some(value) {}

// 2
if self.value is Some(value) {}

// 3
if self.value matches Some(value) {}

I would prefer 1 or 2 personally, but all 3 are an improvement over if let in my opinion

I don't think you will have a lot of success of changing this because removing if let would be a breaking change and have two constructions that only differ in one keyword of syntax seem a bit much.

Furthermore, there is some thoughts that let x = ... could be an expression evaluating to a boolean itself has been brought up.

11 Likes

They're not as different as you might think! let statements can pattern match in the same way that match or if let can:

enum Example {
    Data(i32),
}

let x = Example::Data(123); // Wrap the data.
let Example::Data(y) = x;   // Unwrap ('destructure') the data via a pattern.

println!("{}", y);          // This prints '123'.

The only restriction is that the pattern in the let must be irrefutable - i.e. it must always match successfully. So this would not work:

enum Example {
    Data(i32),
    Oops,
}
    
let x = Example::Data(123);
let Example::Data(y) = x; // error[E0005]: refutable pattern in local binding: `Oops` not covered

println!("{}", y);

This is why the if let syntax was used - it literally is just a let which can fail to bind[1]. I agree that it's maybe not the most intuitive thing at first glance, though.


  1. If you'll excuse the plug, I have a more detailed article on this: Why Did Rust Pick the 'If Let' Syntax? - Joe Clay ↩ī¸Ž

20 Likes

It could be done in an edition.

This is confusing because normally you have a pattern to the left rather than to the right of =>.

1 Like

I'd argue that let Example::Data(y) = x isn't really matching anything though, unlike if let. I'd classify that as destructuring, not matching. let makes me think set this to this, which isn't what if let is for. It might be similar in how it works under the hood maybe, but the concepts feel pretty different.

The fact that let must be infallible is exactly what makes if let unintuitive (as well as it looking like an assignment is used as an expression)

Is the idea there to have let x = Some(value) = self?

You could still have expressions without if let:

let x = match self.value => Some(value);
let x = self.value is Some(value);
let x = self.value matches Some(value);

This is too much churn for an edition, probably, and I don't think for a good reason.

Though there was discussion about using is for general pattern matching, not just within if.

8 Likes

I usually restrict its usage to enums that have only two variants. This way it feels natural in an if statement -- which also has only two branches.

My 2 cents is: if let brings me joy. I wish OCaml had introduced it decades ago: OCaml's let allows non-exhaustive patterns (which can fail at runtime), but Rust forces you to either use an exhaustive pattern (with let) or decide what to do (or do nothing) if it doesn't match (with if let). This is just so great.

7 Likes

With let-else you can use let to match fallible patterns. Likewise, if let can match infallible patterns, and while currently there aren't lot of reasons to do that (mainly temporary lifetime extension, although I generally see match being used for that) with if let chains there will be actual reasons to use infallible patterns in the middle of an if let chain in order to reuse a temporary value in the later steps of the chain.

1 Like

Newbie here. Just an mere observation.

When one begin with Rust, the intuition expects if let .. else destructure some way the all different cases of an enum, so one firstly try with a Result<T> for managing errors; but latter, after trials and searching online one realizes this can be only done with match.

It is like if let .. else were exclusively designed for managing None (From an Option<T> for example) due else{} does not receive any value from the processed enum.

Basically one expects destructure as quickly and clean as possible Result and Option for proceeding to work with the data (without using unwrap()? ), and an if...else have the more comfortable "visuals" due a mere { } across lines.

1 Like

How would you use let-else to match fallible patterns any differently than if let or match? I'm not too sure I understand the point you're making

Are you saying it would bring you less joy to have the syntax changed? This change also wouldn't change anything about exhaustiveness, it's just meant to make it more clear and intuitive

FWIW, the RFCs for if let and while let both cited Swift for precedent. You might also want to review those RFC PRs for their design discussion, including alternative syntax.

2 Likes

They're the same, that's my point. let-else matches a pattern, thus let is not limited to destructuring like you claimed.

1 Like

Alright thanks for explaining. Interesting, I didn't know about let-else or how it works

My immediate reaction to this is that we could probably have

// res is a Result
if let Ok(value) = res {
  // happy
} else let Err(reason) {
  // unhappy
}

It doesn't say else let Err(reason) = res because it only makes sense semantically if you're assigning from the same source in both branches. But the problem is what if there's a lot of code in the happy block? You'd have to scroll up and down looking for the assignment source. Of course this is also a problem with

match res {
   Ok(value) => {
       // happy
   },
   Err(reason) => {
       // unhappy
   },
}

but what that means, I think, is that the only way else let [contextually irrefutable pattern] improves on a match is by having one fewer level of braces and indentation. Which isn't nothing! But it makes me want to look for a change we could make to match to get rid of that extra brace level. Hm. We need some kind of delimiter between the match subject and the first pattern. Maybe reuse the fat arrow?

match res =>
Ok(value) => {
    // happy
},
Err(reason) => {
  // unhappy
},

This hypothetical match block is in tail position of a function, so the parser knows when it's reached the end of the match clauses because the next thing after that trailing comma is another close curly brace. If the match block isn't in tail position then you have to put a semicolon either right after, or instead of, the trailing comma.

Hate? Love? Squirrel?

What about something more or less like this. The only important detail is all happens inside a function, so one needs and can use return if needed.

// res is a Result
// opt is an Option

let data1 = res match Ok or Err(e){ log(e); return Err("Custom err"); }                
let data2 = opt match Some or None { log("failed"); return Err("Custom err"); } 

// now one works with all the destructured datas along the function

Desugaring with other example:

let mut withdata3 = true;
let mut withdata4 = true;
let default = 10;

let data3 = res match Ok or Err(e){ withdata3 = false; default }
let data4 = opt match Some or None {  withdata4 = false; default }

// is:
let data3 = match res {
    Ok(v)  => v,
    Err(e) => { 
		withdata3 = false; 
		default
    },
};

let data4 = match opt {
    Some(v) => v,
    None() => { 
		withdata4 = false; 
		default
    },
};

Another example:

let data = res match Ok or Err(e){

	res2 match Ok or Err(e2){ 
		log(e, e2);
		return Err("res and res2 failed"); 
	}
}

// is:

let data = match res {
    Ok(v)  => v,
    Err(e) => { 
		match res2 {
			Ok(v)  => v,
			Err(e2) => { 
				log(e, e2);
				return Err("res and res2 failed"); 
			},
		}
    },
};
if function() match let Ok(value){
//
}

What you want is on its way to stable for the case where you don't need the other variant's data:

let Some(data2) = opt else { log("failed"); return Err("Custom err"); };
1 Like

I feel like your proposal provides only minimal syntactic sugar for something that I already think of as ergonomic.

It is much more of a hassle to implement and document the new syntax you are proposing.

How is it going to work with struct variants/variants with more than one field? What if my enum has 3 variants?

So all in all I feel like too little is gained over what we already have.