RFC for `if let` expression #160

Merged
merged 2 commits into from Aug 27, 2014

Projects

None yet
@kballard
Contributor
kballard commented Jul 9, 2014
  • Start Date: (fill me in with today's date, YYYY-MM-DD)
  • RFC PR #: (leave this empty)
  • Rust Issue #: (leave this empty)

Summary

Introduce a new if let PAT = EXPR { BODY } construct. This allows for refutable pattern matching
without the syntactic and semantic overhead of a full match, and without the corresponding extra
rightward drift. Informally this is known as an "if-let statement".

Motivation

Many times in the past, people have proposed various mechanisms for doing a refutable let-binding.
None of them went anywhere, largely because the syntax wasn't great, or because the suggestion
introduced runtime failure if the pattern match failed.

This proposal ties the refutable pattern match to the pre-existing conditional construct (i.e. if
statement), which provides a clear and intuitive explanation for why refutable patterns are allowed
here (as opposed to a let statement which disallows them) and how to behave if the pattern doesn't
match.

The motivation for having any construct at all for this is to simplify the cases that today call for
a match statement with a single non-trivial case. This is predominately used for unwrapping
Option<T> values, but can be used elsewhere.

The idiomatic solution today for testing and unwrapping an Option<T> looks like

match optVal {
    Some(x) => {
        doSomethingWith(x);
    }
    None => {}
}

This is unnecessarily verbose, with the None => {} (or _ => {}) case being required, and
introduces unnecessary rightward drift (this introduces two levels of indentation where a normal
conditional would introduce one).

The alternative approach looks like this:

if optVal.is_some() {
    let x = optVal.unwrap();
    doSomethingWith(x);
}

This is generally considered to be a less idiomatic solution than the match. It has the benefit of
fixing rightward drift, but it ends up testing the value twice (which should be optimized away, but
semantically speaking still happens), with the second test being a method that potentially
introduces failure. From context, the failure won't happen, but it still imposes a semantic burden
on the reader. Finally, it requires having a pre-existing let-binding for the optional value; if the
value is a temporary, then a new let-binding in the parent scope is required in order to be able to
test and unwrap in two separate expressions.

The if let construct solves all of these problems, and looks like this:

if let Some(x) = optVal {
    doSomethingWith(x);
}

Detailed design

The if let construct is based on the precedent set by Swift, which introduced its own if let
statement. In Swift, if let var = expr { ... } is directly tied to the notion of optional values,
and unwraps the optional value that expr evaluates to. In this proposal, the equivalent is if let Some(var) = expr { ... }.

Given the following rough grammar for an if condition:

if-expr     = 'if' if-cond block else-clause?
if-cond     = expression
else-clause = 'else' block | 'else' if-expr

The grammar is modified to add the following productions:

if-cond = 'let' pattern '=' expression

The expression is restricted to disallow a trailing braced block (e.g. for struct literals) the
same way the expression in the normal if statement is, to avoid ambiguity with the then-block.

Contrary to a let statement, the pattern in the if let expression allows refutable patterns. The
compiler should emit a warning for an if let expression with an irrefutable pattern, with the
suggestion that this should be turned into a regular let statement.

Like the for loop before it, this construct can be transformed in a syntax-lowering pass into the
equivalent match statement. The expression is given to match and the pattern becomes a match
arm. If there is an else block, that becomes the body of the _ => {} arm, otherwise _ => {} is
provided.

Optionally, one or more else if (not else if let) blocks can be placed in the same match using
pattern guards on _. This could be done to simplify the code when pretty-printing the expansion
result. Otherwise, this is an unnecessary transformation.

Due to some uncertainty regarding potentially-surprising fallout of AST rewrites, and some worries
about exhaustiveness-checking (e.g. a tautological if let would be an error, which may be
unexpected), this is put behind a feature gate named if_let.

Examples

Source:

if let Some(x) = foo() {
    doSomethingWith(x)
}

Result:

match foo() {
    Some(x) => {
        doSomethingWith(x)
    }
    _ => {}
}

Source:

if let Some(x) = foo() {
    doSomethingWith(x)
} else {
    defaultBehavior()
}

Result:

match foo() {
    Some(x) => {
        doSomethingWith(x)
    }
    _ => {
        defaultBehavior()
    }
}

Source:

if cond() {
    doSomething()
} else if let Some(x) = foo() {
    doSomethingWith(x)
} else {
    defaultBehavior()
}

Result:

if cond() {
    doSomething()
} else {
    match foo() {
        Some(x) => {
            doSomethingWith(x)
        }
        _ => {
            defaultBehavior()
        }
    }
}

With the optional addition specified above:

if let Some(x) = foo() {
    doSomethingWith(x)
} else if cond() {
    doSomething()
} else if other_cond() {
    doSomethingElse()
}

Result:

match foo() {
    Some(x) => {
        doSomethingWith(x)
    }
    _ if cond() => {
        doSomething()
    }
    _ if other_cond() => {
        doSomethingElse()
    }
    _ => {}
}

Drawbacks

It's one more addition to the grammar.

Alternatives

This could plausibly be done with a macro, but the invoking syntax would be pretty terrible and
would largely negate the whole point of having this sugar.

Alternatively, this could not be done at all. We've been getting alone just fine without it so far,
but at the cost of making Option just a bit more annoying to work with.

Unresolved questions

It's been suggested that alternates or pattern guards should be allowed. I think if you need those
you could just go ahead and use a match, and that if let could be extended to support those in
the future if a compelling use-case is found.

I don't know how many match statements in our current code base could be replaced with this
syntax. Probably quite a few, but it would be informative to have real data on this.

@kballard kballard referenced this pull request in rust-lang/rust Jul 9, 2014
Closed

Add a matches!(expr, pattern) macro. #14685

@liigo
Contributor
liigo commented Jul 9, 2014

I like this!

@chris-morgan
Member

This has interesting potential. Where I might currently write

match foo {
    A { .. } => {
        a
    }
    B(..) => {
        b
    }
    C => {
        c
    }
    _ => {
        d
    }
}

I could now write

if let A { .. } = foo {
    a
} else if let B(..) = foo {
    b
} else if let C = foo {
    c
} else {
    d
}

Exhaustiveness checking wouldn’t come with this way of doing it, though. (Leastways, not easily, and making it an error would be distinctly suspect.)

I think you’re probably right about guards. Although it could potentially be nice to have them, if let PAT if GUARD = EXPR is just a bit too icky.

👍 from me.

@sinistersnare

I don't know if I like this, it adds unneeded complexity to the language, and is just sugar for a time in this language where we do not need/want sugar (of course this could be a post 1.0 thing then disregard that part). Also it is not the easiest thing to grok, it confused me for a good bit. I also dislike the syntax, but if it gets accepted I do not think I will complain very much.

-1

@nielsle
nielsle commented Jul 9, 2014

Just a nitpick: If optVal is an option, then you can also do

optVal.map( |x| 
    do_something_with(*x)
);
@kballard
Contributor
kballard commented Jul 9, 2014

@nielsle Only if using a closure isn't a problem, which it often can be, and if you have no need for an else clause.

@kballard
Contributor
kballard commented Jul 9, 2014

@sinistersnare What do you mean, a time in this language where we do not need/want sugar? for loops are sugar. Do you think those aren't helpful?

@sinistersnare

The way I see it, we are striping the language to its bare essentials and necessary features for 1.0, and working out ergonomics from there.

As you said, this could be easily desugared into a match block, whereas a for loop is a classic idiom from many programming languages. I consider the way for loops are done in Rust a huge selling point for the language, safer, faster, and better looking.

I just do not think this would be greatly beneficial to Rust, but if the majority opinion says go, sure why not.

@bachm
bachm commented Jul 9, 2014

To me it seems the benefit of this is essentially not having to write the None => {} case in a match. In other words, we want a non-exhaustive version of match, which seems like the simpler solution. Let's call it select:

select optVal {
    Some(x) => doSomethingWith(x)
}
@netvl
netvl commented Jul 9, 2014

@bachm, I think the main benefit is that there is no extra nesting and indentation here. Your select will still need additional level of indentation.

The proposal looks great, +1 from me.

@stepancheg

I always wanted something like this.

BTW, I'd like to propose an alternative: is-expression. It looks like this:

if opt is Some(ref v) { ... }

is-expression in contrast to if-let:

  • looks more natural
  • allows mix of any expressions in if-cond, like:
if (has_next_in_buf() || fetch_from_stream()) && next() is Identifier(name) { ... }

is-expression can be used anywhere deep inside of any expression, like this:

foo(bar is [20, ..]) // pass true if bar is a slice starting with 20, otherwise pass false

However, pattern element can be bound to variable only if is-expression is in &&-argument of if-cond, thus this is invalid (or probably a warning):

if foo is Some(x) || bar is None { ... } // x is not in the scope of then-block

Somewhat similar is-expression is present in Kotlin programming language. Their is expression does two things:

  • checks instance type
  • does smart cast: inside of then-expression type of left argument of is is adjusted
@dobkeratops

alternatives - could macros be beefed up to reduce rightward drift. Imagine if you could do this...

expr.macro!(....) // macro with receiver, comes in as $self  .. 
macro! (..) { ... }   // macro with multiple bracket types, separate's its arguments more

opt.if_is!(Some(x)) { .. do stuff with x.. }

you could build macro forms that fit in more naturally, and have more ways of fighting rightward drift.
(here the motivation for a 'receiver' is approximating infix, not dispatch)

Other than that... to me it does seem sensible to borrow ideas from swift - the language will be very widespread. And "if let ...." certainly makes sense to me coming from C++ where we can write if (auto p=dynamic_cast<Foo*>(expr)) { ... do stuff with p.. }.

@glaebhoerl
Contributor

+1 to the original proposal just as it is. I love it when someone submits the same idea I was going to.

@kballard
Contributor
kballard commented Jul 9, 2014

@stepancheg An is operator like your proposing seems appropriate for pattern-matching, but it doesn't feel to me like it's appropriate for producing a let-binding as a result of destructuring. Especially if it's part of a && chain. For matching alone, your is operator looks basically like the proposed matches!() from rust-lang/rust#14685.

@stepancheg

@kballard no, is looks nothing like matches!(). matches!() doesn't bind variables, unlike is:

if foo is Some(ref s) && bar is Some(ref t) {
    println!("both foo and bar are some: {}, {}", s, t);
}
@kballard
Contributor
kballard commented Jul 9, 2014

@stepancheg Right, that's why I said "for matching alone". As I stated, I don't think it's appropriate to bind variables with an is statement, that feels extremely surprising and counterintuitive.

@zwarich
Contributor
zwarich commented Jul 9, 2014

@stepancheg Would you really want to allow multiple is instances in the same if? Then you could do things like

if foo is A(ref s) || bar is B(ref t) {
...
}

You have to bind every variable exactly once on every path through the conditions. The if let syntax makes this more clear and integrates the existing linearity checking for patterns.

@stepancheg

@zwarich Multiple is separated by || should be allowed similarly to how multiple alternatives are allowed in match: all alternatives must fill the same variable:

struct Foo(int, int);

fn bar(foo: Foo) {
    match foo {
        Foo(1, x) | Foo(y, 2) => {}, // error
        Foo(3, x) | Foo(x, 4) => {}, // OK
        _ => {},
    }
}

fn baz(foo1: Foo, foo2: Foo) {
    if foo1 is Foo(1, x) || foo2 is Foo(y, 2) { ... } // error
    if foo1 is Foo(3, x) || foo2 is Foo(x, 4) { ... } // OK
}
@zwarich
Contributor
zwarich commented Jul 9, 2014

@stepancheg So what does the grammar for your proposed construct look like? You have to add a new nonterminal that is neither an expression nor a pattern.

@stepancheg

@zwarich

You have to invent a new nonterminal that is neither an expression nor a pattern

Sorry, didn't understand that part.

So what does the grammar for your proposed construct look like?

is-expr should be a regular expression. Grammar could be something like this:

if_cond: or_expr
or_expr: and_expr ('||' and_expr)*
and_expr: not_expr ('&&' not_expr)*
not_expr: '!'? (comparison | is_expr)
comparison: ... // down to term
is_expr: expr 'is' match_pat
match_pat: ... // used in match grammar

There's no special treatment of if ... is at parser level. However, typechecker must ensure that either each variable bound in is_expr is either anonymous (e. g. Some(_)) or a part of if_conf and assigned exactly once.

@kballard
Contributor

@stepancheg You haven't addressed my primary objection to is, which is that binding a new variable on the right-hand side of an operator is excessively strange. It also produces a scope that's quite hard to track, because presumably if x is Some(y) && y == 13 { ... } should be valid, but if (x is Some(y) && y == 13) && foo(y) { ... } must be illegal. So the bound variable is visible to all subsequent expressions chained from &&, but it's not visible outside an enclosing expression (e.g. the () group), and it also can't be visible to any subsequent patterns chained with ||. But conversely it is again visible to the body of the if statement. And even considering how other non-boolean expressions interact is confusing.

I think this boils down to conflating an expression with a construct that has the power to bind a variable. Today the only ways to do that are with a declaration (an item or a slot (e.g. a let-binding)) or as part of a match arm, in which any bound variables cannot escape the associated match expression (or even the match arm, i.e. into other match arms). But you're trying to define an expression that does explicitly leak its let-bindings out into the surrounding code, but only under very specific circumstances (i.e. when used in the conditional of an if expression, depending on certain rules around && and || and the complete lack of other operators or expressions being involved). This makes it extremely hard to reason about or to predict the behavior of any complex bit of code that uses the operator.

@stepancheg

@kballard

You haven't addressed my primary objection to is, which is that binding a new variable on the right-hand side of an operator is excessively strange.

Well, I must admit, is is at least unusual, and precise rules of is are significantly more complex than rules of if let.

Probably, all those complex rules are not really needed in practice (or not needed in the first version). If so, I have much simpler, bikeshed, proposal: take yours proposal, and replace

if let PAT = EXPR { BODY }

with

if EXPR is PAT { BODY }

It is almost as simple, as if let, and has advantages:

  • it can be upgraded to full-featured is expression if there will be demand for it
  • it is not confusing. If-let looks like C' assign and use if (a = expr) { ... }, but it is not.
@kballard
Contributor

@stepancheg That is certainly much cleaner than your previous approach, but here are the downsides:

  1. It prevents using is as an operator. You cannot take this proposal and expand it to a full-featured is operator without reintroducing all of the issues I raised before (most importantly, the very bizarre rules around the scope of any bound variables).
  2. It contradicts all existing mechanisms for binding variables in that the bound variables appear on the right-hand side of the delimiter instead of the left-hand side, e,g if foo() is Some(x). The natural way to read this is to treat x as a pre-existing variable and match on its value, rather than treating it as a new let-binding.
  3. It reintroduces ambiguity with braced blocks and struct patterns. if let Foo { x } = foo() { ... } is legal with my proposal, but yours makes that if foo() is Foo { x } { ... } and that's an ambiguous parse. It's currently illegal to wrap a struct pattern like that in parentheses so you cannot resolve this as if foo() is (Foo { x }) { ... } without changing how patterns are parsed.

All in all, I'd rather treat is as a potential operator with the semantics of the matches!() macro from rust-lang/rust#14685.

@stepancheg

@kballard

  1. It reintroduces ambiguity with braced blocks and struct patterns

That's a problem, thanks for pointing it out.

@Valloric

This RFC is excellent. We need to improve Rust's ergonomics; lately it's been going down.

@nielsle
nielsle commented Jul 10, 2014

Perhaps "is" could be replaced by @ in the version proposed by @stepancheg . That would be somewhat consistent with match statements and it would not require a new keyword..

if foo  @ Some(x) {
    doSomethingWith(x)
}

EDIT: Changed => to @

@hatahet
hatahet commented Jul 10, 2014

I like the fact that it is easy to visually notice a match expression, and immediately be able to tell that there is an exhaustive matching going on. It may be harder to do with if let expression. Furthermore, how would this interact with HKT + do expressions? Wouldn't the latter reduce the "burden" of plain match expressions?

@kballard
Contributor

@hatahet Well, the point of an if let is that it's not exhaustive, so I guess I'm not sure what point you're trying to make.

As for HKT + do, could you elaborate? I'm unaware of any concrete proposals for HKT in Rust, or for do expressions, so I can't very well talk about how if let would interact. But since if let is isomorphic to a subset of match expressions, I don't expect there to be any conflict with any future Rust proposals (as the functionality of match is pretty well established at this point and is exceedingly unlikely to change in any meaningful fashion).

If HKT + do extends the behavior of match in any fashion, it's plausible that if let could be similarly extended, but that depends entirely on what changes HKT + do actually makes to match.

@SimonSapin
Contributor

if let is great. +1

@cburgdorf

+1

@stepancheg

@kballard

OK, I came up with another bikeshed proposal: take your proposal, and replace

if let PAT = EXPR { BODY }

with

if EXPR is PAT => { BODY }

New syntax/semantics has following properties:

  • if .. is solves parser ambiguity in my previous proposal
  • fixes problem that let xxx = yyy means different things in different contexts (exhaustive vs. non-exhaustive matching)
  • looks more natural than if let
  • Swift has if let with completely different meaning
  • order of EXPR and PAT is the same as in match expr, unlike if let statement
  • => is the same as in match expression

Addressing two your other issues:

  1. is expression that does not bind variables has little value. In another words, is-expression should either bind variables, or not exist at all.
  2. in if ... is order or EXPR and PAT is the same as in match expr
@kballard
Contributor

@stepancheg What concrete problems do you see with if let that you are trying to solve? You said yours "looks more natural than if let", but that's purely subjective, and I happen to disagree.

Your new proposal doesn't solve any of my big objections to your previous ones, which is the usage of is as if it were an operator (which it isn't, and can't ever be with this proposal), the fact that variables are bound on the RHS which is inconsistent with the rest of Rust and also quite unintuitive, and the fact that your syntax does not make it obvious that this is not a normal if statement.

I'm happy to go into detail on all of this, but this is ground we've covered before and I don't think it's worthwhile. So here's a short rebuttal of the brand new points you've made:

  • let xxx = yyy being generalized from irrefutable to refutable patterns is the entire point of having this construct. I wholly disagree with your statement that it's an advantage to avoid this. I believe this generalization is in fact an intuitive result of the idea of combining let and if together.
  • Swift's if let is very nearly the same meaning. It is a specialization of the if let I've proposed here, behaving as though it were my proposed if let with an implicit Some(...) wrapped around the pattern. Swift's if let foo = bar() { ... } is the same as my if let Some(foo) = bar() { ... }
  • order of EXPR and PAT are not the same as match. match is a hierarchical construct, where the EXPR occurs at a higher level, and all the PATs occur inside a nested block. Your construct puts them left-to-right at the same hierarchical level.
  • I completely disagree that an is operator that doesn't bind variables has little value. In fact, I'm rather tempted to write up an RFC for an is operator as well (although I'll hold off for the time being). Such an operator would obsolete all the type-specific enum variant tests, e.g. Option.is_some() and Result.is_ok(), it would provide a mechanism for doing easy enum variant testing without deriving Eq (which is important for enums that cannot easily derive Eq), and it would provide an easy way for doing equality testing of values embedded (perhaps several levels deep) in enums without having to do a destructuring bind first. As a trivial example, write_err is Err(IoError{ kind: EndOfFile, .. }).

I appreciate the thought you've put into this, but I'd rather talk about what problems you think my proposal has (and why you think they're problems), than continue to bikeshed alternative syntaxes.

@Valloric

In fact, I'm rather tempted to write up an RFC for an is operator as well [...]

@kballard Your description of that potential RFC sounds very nice. Please write that up at some point!

@jfager
jfager commented Jul 11, 2014

*Edit: sorry, posted this before I was done writing it.

Overall I'm +1, but just to clarify from your response to @stepancheg's proposal, this wouldn't allow things like the following, correct?

if let Some(x) = foo && bar {
    doSomethingWith(x)
}

if bar && let Some(x) = foo {
    doSomethingWith(x)
}

if let Some(_) = foo || bar {
    doSomething()
}

That seems to be the drawback to me, that people will see if and want an arbitrary conditional.

You could almost simulate the && cases, except you lose the short-circuiting:

if let (Some(x), true) = (foo, bar) {
    doSomethingWith(x)
}

But there's no way to simulate the || case.

@stepancheg

@kballard

What concrete problems do you see with if let that you are trying to solve?

Largest problem with if let is that it is counter-intuitive.

let x = y;

is a statement, unconditional assignment. It is not an expression, and x must be exhaustive pattern.

However, when let x = y is attached to if:

if let x = y { ... }

meaning of let x = y part becomes different: it is boolean expression now (not a real boolean expression, but it feels like that), and x is no longer required to be exhaustive.

The same syntax construct let ... = ... has different meaning in different contexts. I think it is a problem.

@dobkeratops

So its more like if_let ... than if (let...) .. but it works for me, its similar to reading if (auto *p=...){ .. use p..}, and its kind to all the people who will be learning swift due to the popularity of iOS

@jsanders

I don't think @dobkeratops meant to suggest this, but if this is confusing to some people (so far just @stepancheg) as two keywords combined into one behavior that is different from either of their separate behaviors, what about introducing a new keyword ifletthat does just this? I personally find if let nicer and the swift precedent is a good thing.

@kballard
Contributor

@jfager: Correct, you can't mix && or || into this. The || case would actually be doable with the theoretical is operator. The && case can be solved by creating a free function (say, in std::option) that turns a bool into an Option<()> so you can then use Option.and() to get the short-circuiting && behavior.

Remember, if let is isomorphic to a subset of match. You can't do short-circuiting && or || with match either.

@dobkeratops @jsanders Yes it's effectively an iflet command, except it's written as if let, which I prefer.

@jsanders

Yeah, I agree. I don't think there are any other compound keywords like that (notable, else if instead of elseif).

@kballard
Contributor

@jsanders There are certainly constructions made out of multiple keywords in a row, e.g. extern crate, although I assume what you really mean is two keywords that work independently, behaving differently when joined (e.g. crate doesn't work independently of extern). Of course you already pointed out else if.

And of course we do have precedent for keywords behaving differently in different contexts, e.g. extern { fn foo(); } is quite different than fn foo { ... }.

At some point I expect the old enum mod proposal to be reintroduced as an RFC (though I think that's generally considered a post-1.0 thing at this point). That would be another example of a compound keyword behaving differently than the two keywords do independently.

@jsanders

@kballard I was actually trying to say the same thing as you. There are keyword combinations like if let and else if and others you listed, but not any (that I know of / could find) where the keyword itself is compound like iflet or elseif. So it's an argument for your proposal and against my suggestion.

And yeah, good point that there is precedent for using two keywords together to create a different meaning.

@kballard
Contributor

@jsanders Ah hah I see what you're saying now. And yes you're right, we have no precedent for creating a portmanteau keyword (nor do I want to set such precedent).

@SimonSapin
Contributor

Would if let allow multiple |-separated patterns, like match does? For example:

enum CSSToken {
    Identifier(String),
    Number(f32),
    QuotedString(String),
    Url(String),
    // ...
}

// parse @import
if let Url(value) | QuotedString(value) = token {
    // ...
}

Each alternative would need to bind the same set of names with the same types, but that is already the case with match.

@kballard
Contributor

@SimonSapin Initially, no. I'm open to the idea of adding that later, but I thought it was better to start simple. let doesn't allow alternate patterns so for now if let doesn't either. You can always use a match if you need that kind of behavior.

@SimonSapin
Contributor

Fair enough.

@blaenk
Contributor
blaenk commented Jul 15, 2014

This is very nice. It's one feature that I really liked about Swift, and this proposal is even more flexible than Swift's. +1

@hatahet
hatahet commented Jul 16, 2014

@kballard I haven't seen any concrete proposals either, but do expressions seem to be "unofficially" on the roadmap. (e.g. http://blog.octayn.net/blog/2014/07/15/state-of-rust-0.11.0/).

I guess my main concern is that if they do get implemented, it would add another redundant way of achieving the same thing. E.g.

if let Some(x) = foo() {
    doSomethingWith(x)
}

and

do {
  x <- foo()
} {
  doSomethingWith(x)
}

are equivalent. Of course, the proposal also covers default cases, and multiple match types; however, how much code out there would really make use of this?

@thehydroimpulse

@hatahet I'm pretty sure it that list was about things that were removed or changed. I haven't officially proposed it yet, but I'll be proposing a do macro (instead of the use of a keyword) after HKTs.

Even with a potential do macro in the future, I think sugar like this proposal is good because it could work with arbitrary types, whereas do! would only work on monadic types.

@treeman
treeman commented Jul 16, 2014

I like this. +1

@kballard
Contributor

@hatahet As @thehydroimpulse said, "do expressions" on that post are actually a feature that was removed. It used to be that do foo { bar } was the same thing as foo(|| { bar }), i.e. a simpler invocation syntax for simpler closures. This ended up being pretty much used exclusively as do spawn { ... } and the limited usage, coupled with the changes for procs, resulted in the decision to just remove it entirely.

@liigo
Contributor
liigo commented Jul 16, 2014

If let is great idea, +1
2014年7月17日 上午3:21于 "Jonas Hietala" notifications@github.com写道:

I like this. +1


Reply to this email directly or view it on GitHub
#160 (comment).

@liigo
Contributor
liigo commented Jul 16, 2014

Another alternative: if exp match pat {}.
I like if-match more than if-let currently.

@stepancheg

@liigo AFAIU you cannot unambiguously parse if match, but you can parse if is =>, see my comment above.

@kballard
Contributor

@liigo if exp match pat {} has all the same problems that if exp is pat => {} has except for the one about not being able to use is as an operator (I mean, technically we could still use match as an operator, but it would be very confusing, so that's not really a concern).

@hatahet
hatahet commented Jul 17, 2014

@thehydroimpulse @kballard Thanks for clearing it up, yeah it's a list of things that changed as you pointed out though I definitely do recall coming across discussions about HKT (must have been either on IRC or on the mailing list). In any case, I take note that this is more general than do which only works on monadic types.

@thehydroimpulse

@hatahet np. Yes, HKTs are being discussed a considerable amount recently :)

@DiamondLovesYou

I am firmly against this proposal in principle, not counting HKT. Outside of Option, it's delegation of value handling introduces the possibility of moving errors to potentially obscure places that in all probability won't easily yield the true cause of the error. Furthermore, even with catchalls, like match something { _ => fail!(), }, the error is immediate, even if also at runtime.

If added to librustc, I would bar all of my projects from being allowed utilize it (ie denied with a lint) for the above reason alone.

-1.

@kballard
Contributor

@DiamondLovesYou I don't understand your complaint. Can you please elaborate? In what way does this move errors to potentially obscure places, or actually just move errors at all? And there's no fail!() used here.

@erickt
erickt commented Jul 17, 2014

@kballard: if I interpret @DiamondLovesYou's argument correctly, he's concerned about making it easier to ignore enum variants, which then could lead to bugs as new variants are added. While we have this risk with wildcard match arms, making this syntax shorter will probably lead users to using if let instead of just properly handling all the variants.

I'm still in favor of if let, but it's a valid concern, so I could get behind figuring out how to mitigate this risk. A lint is an obvious choice, but so could constraining this to only working with the Option<T> type. I expect most of the users of this pattern will be for Option<T> types, so that might not be too bad of an idea.

@sinistersnare

maybe if we required an else branch if there are other variants to be tested?

@pczarn
pczarn commented Jul 17, 2014

We could do an exhaustiveness check for non-nullary variants. Consequently constraining it to Option and similar types.

@hatahet
hatahet commented Jul 17, 2014

@DiamondLovesYou @kballard This is basically what I was trying to say when I mentioned the non-exhaustiveness aspect of this proposal. The nice thing about match is that it does force you to handle all cases; and that using a wildcard match is explicit. Perhaps requiring an else branch as @sinistersnare mentioned to achieve the same behavior?

@jfager
jfager commented Jul 17, 2014

I don't understand this concern at all. This is a conditional bind on a single explicit pattern, if the pattern doesn't match the condition isn't satisfied, the bind doesn't happen and the block doesn't execute. Why do other variants matter if you're explicitly asking for a particular one?

if foo == EnumVarA potentially isn't exhaustive, either, should explicit enum comparisons in regular conditionals be removed or linted as well?

@kballard
Contributor

This is a conditional bind on a single explicit pattern, if the pattern doesn't match the condition isn't satisfied, the bind doesn't happen and the block doesn't execute.

Exactly.

I feel like the suggestions that this is a problem, or that there should be some sort of exhaustiveness check or required else block, are completely missing the entire point of the if let construct. If I'm using if let, it's because I'm very deliberately only testing a single variant of an enum (or some other form of non-exhaustive match). As such, any time you see if let you should be primed already to recognize that the match is non-exhaustive and that other cases are not handled.

@glaebhoerl
Contributor

I think 90% of the use cases for if let will just be with Option and Result. No need to overthink it. If match is more appropriate in a particular case, use match. If if let is more appropriate, use that. Maybe add some kind of lint if people are really concerned about accidents with if let (I don't personally see it), but that's all.

@DiamondLovesYou

@erickt: Nailed it. W.r.t your second point, there's no point to attempting to mitigate the risk: match already solves the problem. That said, I'll acknowledge that match is slightly more verbose than it could (from a immediate term productivity POV only) be in the case of Options. However, given the multitude of ways to safely transform an Option without ever having to use a match or otherwise explicitly account for an Option's Some or None status, there's very little reason and productivity harm to make this proposal's benefits outweigh its costs.

@DiamondLovesYou

@kballard: I can elaborate; see my last comment.

@kballard
Contributor

@DiamondLovesYou I still don't understand what you think the cost is. The expression

if let Some(foo) = bar {
    ...
}

is equivalent to the expression

match bar {
    Some(foo) => ...,
    _ => {}
}

If you recognize that equivalence, then reading the former should have the same meaning to you as reading the latter. Which means that any time you see if let you automatically know that there's an effective wildcard match that causes non-matching patterns to be ignored.

As near as I can tell, the cost that you're suggesting would only happen if the existence of if let causes people to not bother handling variants that they otherwise would have. But I see no reason to make such a claim. Anyone who's willing to use an if let to ignore a variant they should have handled would have already been willing to do use a wildcard pattern in a match to do the same. And as @jfager has pointed out, we already have the ability to say if foo == SomeNullaryVariant { ... }, and nobody has ever claimed that has been a problem.

@jsanders

@kballard This is dredging up a line of discussion from awhile ago, but could you elaborate on the problems with using a closure? In the example from your last comment:

bar.map(|foo| {
    ...
})

When is using a closure a problem? Is it a performance issue?

@DiamondLovesYou

@kballard: The costs I refer to are hidden, call it 'technical debt'. If this were used and I needed to add a new value to Option (disregarding how insane that seems), more than likely all code using this proposal would break. Silently.

Also, I would say

bar.map(|bar| ... )

is cleaner and shorter than either forms.

W.r.t. "ya know what, I don't need to handle the variants, because I can just use if let": between you, me, and everyone reading this, sure. But these days, software engineering is a team sport. Ergo, it is a reasonable certainty that I would have to make modifications to someones else's code. So, I'd like to be safe while I'm insane (pun intended), even at the expense of upfront productivity.

@hatahet
hatahet commented Jul 17, 2014

@DiamondLovesYou map is one way, and it is also possible to use for as well:

for b in bar.iter() {
   ...
}
@kballard
Contributor

@jsanders Performance is a possibility, but it also just plain behaves differently. For example, you can't say

let v = vec![1u,2,3];
let ret = bar.map(|foo| {
    Some(v)
});

Unboxed by-value closures will allow that particular case, but then you can't add an else condition that uses the same non-copyable value, e.g.

let v = vec![1u,2,3];
let ret = bar.map(|foo| {
    v.append(foo)
}).or_else(|| {
    v
});

And of course the more variables you need to capture, the more likely you are to have issues, either right now due to the by-reference-capturing nature of closures, or in an unboxed closure world, with having to deal with which values to move vs capturing by-ref vs capturing by-val, and how multiple closures that need to capture the same value might interact (e.g. that or_else() case).

Also, this requires every single enum you want to work with provide its own map(), for every single variant that you care about (or some sort of generic .map_if(VariantName, || { closure }) which doesn't work if the variants contain different types of data).

@DiamondLovesYou

W.r.t. "ya know what, I don't need to handle the variants, because I can just use if let": between you, me, and everyone reading this, sure. But these days, software engineering is a team sport. Ergo, it is a reasonable certainty that I would have to make modifications to someones else's code. So, I'd like to be safe while I'm insane (pun intended), even at the expense of upfront productivity.

You're still making the assumption that the existence of if let would cause people to write code more unsafely than they would otherwise, and I still don't see any evidence for this assumption. We already have multiple tools for ignoring unhandled variants in different cases (e.g. .unwrap(), match with wildcard arm, etc). The existence of if let doesn't change any of that. It just simplifies the two common cases of either very intentionally only caring about one variant, or caring about one variant + wildcard (e.g. with the else block on the if let).

@kballard
Contributor

@hatahet Using a for loop to handle an Option is generally considered to be very poor style, and also isn't usable in a lot of cases (due to the fact that it's a loop). The reason Option supports iterating is actually so you can use it as a single-element iterator for the purposes of chaining with other iterators, not so you can loop over it.

Example of where it fails:

let v = vec![1u,2,3];
for x in opt.move_iter() {
    foo(v.append(x))
}

The compiler (rightfully) says that it can't move v here because it may have already been moved on a pervious iteration of the loop.

@DiamondLovesYou

@kballard:

let v = vec![1u, 2, 3];
let v = v.move_iter().chain(bar.iter()).collect();
v

I suppose I am. However, I believe this is a valid assumption considering the virtue of the proposal is to add the ability for its use in librustc. And, thus, its use in practice (else, why make the proposal?).

(Let us note that our examples here have done well (IMO, verily) without the need for the proposal).

@kballard
Contributor

@DiamondLovesYou

What do you mean, "the ability for its use in librustc"? This proposal is to add the feature to the language. librustc is the implementation of our compiler, yes, but all language grammar/semantics is implemented there (or in supporting libraries like libsyntax). And yes, like all RFCs, the goal is to actually use the thing proposed. There is no point in proposing something that won't be used.

I still don't see how this is relevant to your assumption. You seem to be implying that the very use of if let is somehow unsafe, which is patently false. It is very common today to have constructs like

match foo {
    Some(x) => {
        // do stuff with x
    }
    _ => {}
}

And that is what if let replaces. Same semantics, much nicer usage. It also replaces the similar example where the wildcard match has a body (via the else block). Again, same semantics, much nicer usage.

In fact, I can't actually think of how you're supposed to use this unsafely. Can you give a code example using if let that you a) consider to be unsafe, and b) think that anyone who would write this would not have just used the equivalent match otherwise?

(Let us note that our examples here have done well (IMO, verily) without the need for the proposal).

What do you mean? My examples are all of code that doesn't work today. And they're demonstrating the flaws in the examples that you and others have posted.

@jsanders

@kballard Thanks for the detailed elaboration, makes sense.

@hatahet
hatahet commented Jul 17, 2014

@kballard

Using a for loop to handle an Option is generally considered to be very poor style, and also isn't usable in a lot of cases...

Thanks for the clear up.

@kballard
Contributor

The costs I refer to are hidden, call it 'technical debt'. If this were used and I needed to add a new value to Option (disregarding how insane that seems), more than likely all code using this proposal would break. Silently.

Nearly all code without this proposal would also break. if let wouldn't break any more than the multitude of other ways of handling Option would.

It sounds to me like your argument is a reformulation of the argument against using default in a C switch. But Rust isn't C, and the reasons to avoid default when dealing with enumerations in C don't apply to Rust. Similarly, the reasons for using default in C don't apply to Rust either (e.g. in Rust, an enum value must be one of its variants; in C, an enum value could in fact be any integral value that fits in the underlying primitive type).

The only situation in which if let could plausibly be used that would break in this fashion is with something like

enum ErrorCode {
    Code1(uint),
    Code2
}

if let Code1(x) == foo() {
    // handle Code1
} else {
    // handle Code2
}

i.e. a bi-variant enumeration that has no intrinsic semantic need to remain bi-variant (as Option and Result do) and therefore may gain new variants over time. But I don't think it's reasonable to expect anyone to ever use if let in this situation. They will just do the same thing they do today, which is

match foo() {
    Code1(x) => { /* handle Code1 */ }
    Code2 => { /* handle Code2 */ }
}

In the end, if let is only going to be used when the equivalent match makes sense, i.e. when you expressly only care about one variant.

@hatahet
hatahet commented Jul 17, 2014

@kballard

But I don't think it's reasonable to expect anyone to ever use if let in this situation.

I think this is the point of contention here :) Programmers will use whatever feature exists in the language (see C++). It remains a question to be answered when it comes this proposal, but why open this door anyway? My guess is that programmers not familiar with sum types would probably go this route since it is more in line to what they are already familiar with, but that's not necessarily the best way.

@kballard
Contributor

@hatahet The door is already open.

match foo {
    Code1 => { /* handle Code1 */ }
    _ => { /* handle Code2 */ }
}

Also, I disagree that this makes sense to programmers not familiar with sum types. It's simply not normal for a C programmer to write

switch (foo) {
    case Code1: /* handle Code1 */ break;
    default: /* handle Code2 */
}

What is more plausible is handling several variants at once in a default case, but that already means you're not talking about a bi-valued type, which already means we've left the proposed scenario.

@hatahet
hatahet commented Jul 17, 2014

@kballard
Perhaps having a lint for the first case you posted then, since the compiler can determine that _ in this case can only match Code2?

@kballard
Contributor

@hatahet If we add if let and we subsequently start seeing problematic code like this, then that might warrant a lint. But I think it's a bit premature to add a lint with no evidence that there's a problem in actual usage.

@kballard kballard referenced this pull request Jul 23, 2014
Closed

Add "with-else" proposal #181

@dhardy
Contributor
dhardy commented Jul 23, 2014

I appear to have reinvented this: #181

Less flexible, more concise, possibly less error-prone (since an Option can only have one non-empty value).

@sinistersnare

@dhardy This proposal is not just for Option types, this is a general destructuring sugar, and because Options are not a language feature, but a library feature, your RFC is likely not going to be accepted.

@dhardy dhardy pushed a commit to dhardy/rfcs that referenced this pull request Jul 23, 2014
Diggory Hardy Take note of RFC #160 b067639
@Manishearth
Member

+1

For those who say that this is non-exhaustive: It's not meant to be. You use it when you explicitly want to test for a single variant (or a small set if you're using the proposed | syntax). Everyone already uses the classic

match foo {
  Bar(x) => {/*...*/},
  _ => {}
}

all over the place, if introducing a new variant breaks if-let, it also breaks these matches.

I agree that a match statement makes you explicitly think about exhaustiveness, but so does if let -- it's pretty clear that you're talking about one variant, and all the rest are to be ignored. I can't imagine any case where newbies/inexperienced programmers would use if let but not match due to the explicit wildcard in match.

@dhardy
Contributor
dhardy commented Jul 28, 2014

+1

Having thought about it a bit, I prefer if let over my own proposal: it is more generic, does not require a new keyword, is already used in another language, and its meaning is somewhat self-explanatory.

I somewhat dislike the use of = to mean both "if PAT matches EXPR" and "let VAR be ...": = is usually only used for assignment. I can think of some alternatives, but none of them are really better: with PAT in EXPR (like in my RFC, inspired by the Python with syntax), with PAT from EXPR (as @kballard pointed out, in is inappropriate; from may be better but I don't think is a good idea), match PAT against EXPR, ... so probably not worth revising.

So, why I think if let or similar is important: it gives match-like semantics with a different logic structure which isn't otherwise available (e.g. see the example in the #181 proposal).

@bharrisau

But it IS assignment. A refutable assignment.

@dhardy
Contributor
dhardy commented Jul 28, 2014

No, it's a conditional and an assignment. And it's definitely not refutable.

@bharrisau

It's a destructuring assignment with a refutable pattern. Unless I'm missing something.

It isn't an if let, it is if let. let requires an irrefutable pattern, if let can have a refutable one. You just need to provide the block to execute if the assignment succeeds (so it looks like an if).

@dhardy
Contributor
dhardy commented Jul 28, 2014

I would rather call it a matchable pattern in a conditional assignment; but if you wish to call it a refutable pattern in a claimed assignment, I guess I can accept that as almost correct English. Not that this argument is going anywhere much.

@glaebhoerl
Contributor

As far as I'm aware "refutable" and "irrefutable" are terms of art, not necessarily used here in their English sense, for patterns which can or cannot fail to match.

@kballard kballard referenced this pull request in rust-lang/rust Aug 1, 2014
Closed

rustdoc: Use --crate-name with --test #16157

@kballard
Contributor
kballard commented Aug 7, 2014

Does anyone feel that there's anything from this discussion that needs to be added back to the RFC document?

@kballard kballard referenced this pull request in rust-lang/rust Aug 25, 2014
Closed

Implement `if let` #16741

@kballard
Contributor

I went ahead and created a patch for this as rust-lang/rust#16741.

@reem
reem commented Aug 25, 2014

I am strongly in favor of this RFC, it makes working with enums, especially those with two variants, much more pleasant.

@kballard
Contributor

It turns out Swift also supports while let. Is there any benefit to adding that to Rust as well, or should we just stick with if let?

@cmr
Contributor
cmr commented Aug 25, 2014

I've often wanted a keyword for the loop { match foo { ... } } construct.

On Mon, Aug 25, 2014 at 7:18 PM, Kevin Ballard notifications@github.com
wrote:

It turns out Swift also supports while let. Is there any benefit to
adding that to Rust as well, or should we just stick with if let?


Reply to this email directly or view it on GitHub
#160 (comment).

http://octayn.net/

@glaebhoerl
Contributor

@kballard Interesting. I'm guessing it would be used like so?

while let Some(val) = iter.next() {
    println!("val: {}", val);
}

I actually like this quite a bit, but (given that the discussion about if let has already run its course) it would probably be better to just get if let in first, and propose this in a new RFC.

@cmr As the simplest possible solution, we could even just make a special exception and allow loop match foo { ... } without the double braces.

@kballard
Contributor

@glaebhoerl Yeah that's how it would work.

loop match foo { ... } looks pretty weird to me. That looks like loop is being used as some sort of prefix modifier, which would suggest it should work for other expressions too.

@mahkoh
Contributor
mahkoh commented Aug 26, 2014

+1

match maybe_x() {
    Some(x) => { ... },
    _ => match maybe_y() {
        Some(y) => { ... },
        _ => { ... },
    },
}

is way too noisy.

@glaebhoerl
Contributor

@kballard Yeah, maybe. But weighed against the alternative of adding a new keyword? More economical to just make loop match be the "keyword". Kind of similar to the logic for if let. (Or of course, could just do nothing and keep requiring nested blocks. Not a formal proposal, just brainstorming "if we want to solve this, how could we solve it" w.r.t. @cmr's desire.)

@kballard
Contributor

What new keyword?

@glaebhoerl
Contributor

Quoth @cmr:

I've often wanted a keyword for the loop { match foo { ... } } construct.

@kballard
Contributor

@glaebhoerl Yes, and that's what the while let ... I mentioned would do. No new keyword, just a desugaring of an existing-but-illegal keyword combination (just like if let).

@alexcrichton
Member

This was discussed in today's meeting.

Concerns were brought up about how a simple AST rewrite can yield surprising results in, but it was also decided that an implementation not specc'd to be an AST rewrite would not be accepted. Due to lingering uncertainties, we decided to merge this with the caveat that it is all initially behind a feature gate. @kballard, can you update the RFC to reflect this?

We also decided to postpone something like while let to a future RFC rather than asking for inclusion in this one.

@kballard kballard Add note about feature-gate
Also fix a couple of typos.
d7a1dba
@kballard
Contributor

@alexcrichton Note about feature gate added.

@alexcrichton alexcrichton referenced this pull request in rust-lang/rust Aug 27, 2014
Closed

Implement if-let #16779

@alexcrichton alexcrichton merged commit d7a1dba into rust-lang:master Aug 27, 2014
@kballard kballard deleted the kballard:if_let branch Aug 27, 2014
@stepancheg stepancheg added a commit to stepancheg/rust-protobuf that referenced this pull request Dec 6, 2014
@stepancheg stepancheg Use `if let` statement in generated code
(I'd like Rust to have more generic `is` expression:
rust-lang/rfcs#160 (comment) )
568a18b
@Biluoshilang

find no goods for it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment