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

RFC: if- and while-let-chains #2260

Closed
wants to merge 1 commit into
base: master
from

Conversation

Projects
None yet
@Centril
Contributor

Centril commented Dec 24, 2017

Rendered

This RFC was co-authored with @scottmcm ;)


Here's a survey on some choices of syntax.
Please answer it if you have the time.
And please don't spread the survey around.

https://goo.gl/forms/Wx61sx9s03N5SGA52


Before considering this RFC, please remember what is most important: reducing rightward drift and improving ergonomics in control flow. The exact syntax can always be changed into a direction you prefer. If you don't like the syntax, please write a comment about that below or agree with someone who has put forth your view. Thank you :)

EDIT: The exact syntax regarding && or , or chaining if is very much in flux right now.


This RFC extends if let- and while let-expressions with chaining, allowing
you to combining multiple lets and conditions together naturally. With this
RFC implemented, you will, among other things, now be able to write:

/// Returns the slice of format string parts in an `Arguments::new_v1` call.
fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if let ExprAddrOf(_, ref expr) = expr.node // &["…", "…", …]
     , let ExprArray(ref exprs) = expr.node
     , let Some(expr) = exprs.last()
     , let ExprLit(ref lit) = expr.node
     , let LitKind::Str(ref lit, _) = lit.node {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
}

@Centril Centril added the T-lang label Dec 24, 2017

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Dec 24, 2017

Contributor

RFC 2046 is a more general control flow graph (CFG) control feature. While it doesn't solve the rightward drift or ergonomic issues that this RFC does, it allows the macros to be improved by removing duplication of else blocks.

RFC #2046 does help you with rightward drift:

'label :{
    let expr = if let ExprAddrOf(_, ref expr) = expr.node {
        expr
    } else {
        break 'label None;
    };
    let expr = if let ExprArray(ref exprs) = expr.node {
        exprs
    } else {
        break 'label None;
    };
    let expr = if let Some(expr) = exprs.last() {
        expr
    } else {
        break 'label None;
    };
    // ...
    Some((lit.as_str(), exprs.len()))
}

Now if you add one simple macro, unwrap_or_none, you can simply it to:

'label : {
    let expr = unwrap_or_none!(ExprAddrOf(_, ref expr) = expr.node, 'label);
    let exprs = unwrap_or_none!(ExprArray(ref exprs) = expr.node, 'label);
    let expr = unwrap_or_none!(Some(expr) = exprs.last(), 'label);
    let lit = unwrap_or_none!(ExprLit(ref lit) = expr.node, 'label);
    let lit = unwrap_or_none!(LitKind::Str(ref lit, _) = lit.node, 'label);
    Some((lit.as_str(), exprs.len()))
}

This allows you to do non-trivial code between the let declarations, something that is very inconvenient with this proposal, so it fixes the ergonomics issues much better than this RFC.

Contributor

est31 commented Dec 24, 2017

RFC 2046 is a more general control flow graph (CFG) control feature. While it doesn't solve the rightward drift or ergonomic issues that this RFC does, it allows the macros to be improved by removing duplication of else blocks.

RFC #2046 does help you with rightward drift:

'label :{
    let expr = if let ExprAddrOf(_, ref expr) = expr.node {
        expr
    } else {
        break 'label None;
    };
    let expr = if let ExprArray(ref exprs) = expr.node {
        exprs
    } else {
        break 'label None;
    };
    let expr = if let Some(expr) = exprs.last() {
        expr
    } else {
        break 'label None;
    };
    // ...
    Some((lit.as_str(), exprs.len()))
}

Now if you add one simple macro, unwrap_or_none, you can simply it to:

'label : {
    let expr = unwrap_or_none!(ExprAddrOf(_, ref expr) = expr.node, 'label);
    let exprs = unwrap_or_none!(ExprArray(ref exprs) = expr.node, 'label);
    let expr = unwrap_or_none!(Some(expr) = exprs.last(), 'label);
    let lit = unwrap_or_none!(ExprLit(ref lit) = expr.node, 'label);
    let lit = unwrap_or_none!(LitKind::Str(ref lit, _) = lit.node, 'label);
    Some((lit.as_str(), exprs.len()))
}

This allows you to do non-trivial code between the let declarations, something that is very inconvenient with this proposal, so it fixes the ergonomics issues much better than this RFC.

@Havvy

This comment has been minimized.

Show comment
Hide comment
@Havvy

Havvy Dec 24, 2017

Contributor

With this, we don't need && anymore.

// This predicate
if a && b && c {}

// can be replaced with this if let chain.
if let () = (), a, b, c {}

The () = () is required because the chain has to start with a pattern.

Contributor

Havvy commented Dec 24, 2017

With this, we don't need && anymore.

// This predicate
if a && b && c {}

// can be replaced with this if let chain.
if let () = (), a, b, c {}

The () = () is required because the chain has to start with a pattern.

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Dec 24, 2017

Contributor

Another alternative to this RFC: if guards on if's:

fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if let ExprAddrOf(_, ref expr) = expr.node // &["…", "…", …]
            if let ExprArray(ref exprs) = expr.node
            if let Some(expr) = exprs.last()
            if let ExprLit(ref lit) = expr.node
            if let LitKind::Str(ref lit, _) = lit.node {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
}

This would be more consistent to what match is doing, and I think there are no parser ambiguities.

Contributor

est31 commented Dec 24, 2017

Another alternative to this RFC: if guards on if's:

fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if let ExprAddrOf(_, ref expr) = expr.node // &["…", "…", …]
            if let ExprArray(ref exprs) = expr.node
            if let Some(expr) = exprs.last()
            if let ExprLit(ref lit) = expr.node
            if let LitKind::Str(ref lit, _) = lit.node {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
}

This would be more consistent to what match is doing, and I think there are no parser ambiguities.

@Wyverald

This comment has been minimized.

Show comment
Hide comment
@Wyverald

Wyverald Dec 24, 2017

I find this syntax very confusing. At a glance this looks very much like parallel if lets, instead of sequential ones -- I was expecting all these if lets to be applied at the same time. But that was obviously not the case as expr.node was being matched against multiple disjoint patterns, so I figured "oh, it must be an or instead of an and" -- and no, it's not that either. (Thankfully -- an or-ing if let chain would be even more troublesome.)

I still do think the best way to deal with this kind of right-ward drift is with early returns, with something like if !let or let pat = expr else. The pros and cons of that approach are extensively discussed in another RFC, so I won't reiterate everything here.

Wyverald commented Dec 24, 2017

I find this syntax very confusing. At a glance this looks very much like parallel if lets, instead of sequential ones -- I was expecting all these if lets to be applied at the same time. But that was obviously not the case as expr.node was being matched against multiple disjoint patterns, so I figured "oh, it must be an or instead of an and" -- and no, it's not that either. (Thankfully -- an or-ing if let chain would be even more troublesome.)

I still do think the best way to deal with this kind of right-ward drift is with early returns, with something like if !let or let pat = expr else. The pros and cons of that approach are extensively discussed in another RFC, so I won't reiterate everything here.

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Dec 24, 2017

Contributor

I agree with @Wyverald that the , syntax is confusing. I wondered at first too whether it was "or" mode or "and" mode (it's "and" mode). I think my suggestion for if like guards is more clear here.

Contributor

est31 commented Dec 24, 2017

I agree with @Wyverald that the , syntax is confusing. I wondered at first too whether it was "or" mode or "and" mode (it's "and" mode). I think my suggestion for if like guards is more clear here.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

@Havvy You certainely could write if let () = (), a, b, c {}, but also if a, b, c {} - but I don't know if I would recommend it for readability purposes.

@Wyverald We did not consider that this could be confused with parallel assignment at all. If that is an inference that many make, we need to change the syntax of course.

@est31 We didn't consider this, and that is true. But the first non-macro snippet you listed is not very ergonomic or readable and has a lot of repetition. The macro does reduce this, but repeats 'label 5 times. I am a firm believer that control flow should be more universal to all rustaceans and not mainly done by macro (we have if_chain, and that works, but I believe we should have this in surface lang instead). Macros are better, in my opinion for things like EDSLs.

I did briefly consider: (and yes, there should be no parser ambiguities)

fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if let ExprAddrOf(_, ref expr) = expr.node // &["…", "…", …]
    if let ExprArray(ref exprs) = expr.node
    if let Some(expr) = exprs.last()
    if let ExprLit(ref lit) = expr.node
    if let LitKind::Str(ref lit, _) = lit.node {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
}

but went on since it is slightly more verbose. However, it aligns pretty well and is a syntax I could live with. I will certainly add it as an alternative if it is not changed to be the main proposal instead.

Contributor

Centril commented Dec 24, 2017

@Havvy You certainely could write if let () = (), a, b, c {}, but also if a, b, c {} - but I don't know if I would recommend it for readability purposes.

@Wyverald We did not consider that this could be confused with parallel assignment at all. If that is an inference that many make, we need to change the syntax of course.

@est31 We didn't consider this, and that is true. But the first non-macro snippet you listed is not very ergonomic or readable and has a lot of repetition. The macro does reduce this, but repeats 'label 5 times. I am a firm believer that control flow should be more universal to all rustaceans and not mainly done by macro (we have if_chain, and that works, but I believe we should have this in surface lang instead). Macros are better, in my opinion for things like EDSLs.

I did briefly consider: (and yes, there should be no parser ambiguities)

fn get_argument_fmtstr_parts(expr: &Expr) -> Option<(InternedString, usize)> {
    if let ExprAddrOf(_, ref expr) = expr.node // &["…", "…", …]
    if let ExprArray(ref exprs) = expr.node
    if let Some(expr) = exprs.last()
    if let ExprLit(ref lit) = expr.node
    if let LitKind::Str(ref lit, _) = lit.node {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
}

but went on since it is slightly more verbose. However, it aligns pretty well and is a syntax I could live with. I will certainly add it as an alternative if it is not changed to be the main proposal instead.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

@est31 If we go with your proposed syntax however, we need to consider what that means for | and what the precedence between | and if is for future-proofing purposes. Any thoughts on this?

Contributor

Centril commented Dec 24, 2017

@est31 If we go with your proposed syntax however, we need to consider what that means for | and what the precedence between | and if is for future-proofing purposes. Any thoughts on this?

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Dec 24, 2017

Contributor

But the first non-macro snippet you listed is not very ergonomic and has a lot of repetition.

Yeah, that's why I added a macro snippet. The first snippet was only to reduce rightward drift.

The macro does reduce this, but repeats 'label 5 times.

You can probably minimize this further by creating a macro inside the 'label block that passes on 'label. Its only an improvement for very long examples though.

If we go with your proposed syntax however, we need to consider what that means for | and what the precedence between | and if is for future-proofing purposes. Any thoughts on this?

I don't have any views/thoughts on the precedence.

Contributor

est31 commented Dec 24, 2017

But the first non-macro snippet you listed is not very ergonomic and has a lot of repetition.

Yeah, that's why I added a macro snippet. The first snippet was only to reduce rightward drift.

The macro does reduce this, but repeats 'label 5 times.

You can probably minimize this further by creating a macro inside the 'label block that passes on 'label. Its only an improvement for very long examples though.

If we go with your proposed syntax however, we need to consider what that means for | and what the precedence between | and if is for future-proofing purposes. Any thoughts on this?

I don't have any views/thoughts on the precedence.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

Yeah, that's why I added a macro snippet. The first snippet was only to reduce rightward drift.

Right. I will update the RFC with a more nuanced description in a while (holidays, so, yeah... ^,^)

You can probably minimize this further by creating a macro inside the 'label block that passes on 'label. Its only an improvement for very long examples though.

Sounds about right. However, the macro is specific to returning None, does the macro method scale to else if and other expressions of other types in else ?

I don't have any views/thoughts on the precedence.

We'll have to resolve this together tho I think unless we're OK with shutting the door to if let A(x) = e1 | B(x) = e1 {..} else {..} (semantically).

Contributor

Centril commented Dec 24, 2017

Yeah, that's why I added a macro snippet. The first snippet was only to reduce rightward drift.

Right. I will update the RFC with a more nuanced description in a while (holidays, so, yeah... ^,^)

You can probably minimize this further by creating a macro inside the 'label block that passes on 'label. Its only an improvement for very long examples though.

Sounds about right. However, the macro is specific to returning None, does the macro method scale to else if and other expressions of other types in else ?

I don't have any views/thoughts on the precedence.

We'll have to resolve this together tho I think unless we're OK with shutting the door to if let A(x) = e1 | B(x) = e1 {..} else {..} (semantically).

@mdinger

This comment has been minimized.

Show comment
Hide comment
@mdinger

mdinger Dec 24, 2017

Contributor

This is a very well written RFC. Thanks for writing it. Well written RFCs really improve this process and are are so very helpful for all involved.

For reference, this topic was previously discussed in #929.

Contributor

mdinger commented Dec 24, 2017

This is a very well written RFC. Thanks for writing it. Well written RFCs really improve this process and are are so very helpful for all involved.

For reference, this topic was previously discussed in #929.

@petrochenkov

This comment has been minimized.

Show comment
Hide comment
@petrochenkov

petrochenkov Dec 24, 2017

Contributor

Short review (I'll probably elaborate later):

  • I support the motivation with all my heart. The current situation is bad.
  • This RFC solves the problem of composability of conditions, but doesn't solve the problem of inverted order. Nobody writes 5 == x in their non-pattern conditions, but analogues of if let 5 = x are for some reason okay in conditions with patterns. I want to write x op 5 in both pattern and pattern-less conditions.
  • This RFC doesn't solve the problem of let not being an expression either. Have you ever seen something like if let VariantX(..) = self { true } else { false }? It's so bad people create separate functions like
    fn is_variant_x(self) {
        if let VariantX(..) = self { true } else { false }
    }
    or generate unnecessary PartialEq impls with derive just to avoid writing it more than once.
  • The logical AND is called , for some reason, I thought && is usually used for it.
  • This is a lot of ad-hoc syntax to deprecate when the proper solution solving all the listed problems is implemented :)
    The example from the motivation section rewritten with is:
    let name = meta.name();
    if set.is_some() {
        error::set_again(name);
    } else if meta is &NameValue(_, Lit::Int(val, ty)) &&
              val <= u32::MAX as u64 &&
              is_unsigned(ty) {
        *set = Some(val as u32); 
    } else {
        error::weight_malformed(name);
    }

Before considering this RFC, please remember what is most important: semantics. The syntax can always be changed into a direction you prefer.

Syntax is the most important aspect in this particular case - the whole point of the change is resyntax nested conditions and conditions with weird inverted order into something nicer looking.

Contributor

petrochenkov commented Dec 24, 2017

Short review (I'll probably elaborate later):

  • I support the motivation with all my heart. The current situation is bad.
  • This RFC solves the problem of composability of conditions, but doesn't solve the problem of inverted order. Nobody writes 5 == x in their non-pattern conditions, but analogues of if let 5 = x are for some reason okay in conditions with patterns. I want to write x op 5 in both pattern and pattern-less conditions.
  • This RFC doesn't solve the problem of let not being an expression either. Have you ever seen something like if let VariantX(..) = self { true } else { false }? It's so bad people create separate functions like
    fn is_variant_x(self) {
        if let VariantX(..) = self { true } else { false }
    }
    or generate unnecessary PartialEq impls with derive just to avoid writing it more than once.
  • The logical AND is called , for some reason, I thought && is usually used for it.
  • This is a lot of ad-hoc syntax to deprecate when the proper solution solving all the listed problems is implemented :)
    The example from the motivation section rewritten with is:
    let name = meta.name();
    if set.is_some() {
        error::set_again(name);
    } else if meta is &NameValue(_, Lit::Int(val, ty)) &&
              val <= u32::MAX as u64 &&
              is_unsigned(ty) {
        *set = Some(val as u32); 
    } else {
        error::weight_malformed(name);
    }

Before considering this RFC, please remember what is most important: semantics. The syntax can always be changed into a direction you prefer.

Syntax is the most important aspect in this particular case - the whole point of the change is resyntax nested conditions and conditions with weird inverted order into something nicer looking.

@matklad

This comment has been minimized.

Show comment
Hide comment
@matklad

matklad Dec 24, 2017

Member

@petrochenkov ohhh, this is looks sweet! Could we also have a cond/when/multibranch if :) ?

let name = meta.name();
when {
    set.is_some() => 
        error::set_again(name),

    meta is &NameValue(_, Lit::Int(val, ty)) && 
    val <= u32::MAX as u64 && 
    is_unsigned(ty) => 
        *set = Some(val as u32),

    else => 
        error::wiegt_malformed(name),
}
Member

matklad commented Dec 24, 2017

@petrochenkov ohhh, this is looks sweet! Could we also have a cond/when/multibranch if :) ?

let name = meta.name();
when {
    set.is_some() => 
        error::set_again(name),

    meta is &NameValue(_, Lit::Int(val, ty)) && 
    val <= u32::MAX as u64 && 
    is_unsigned(ty) => 
        *set = Some(val as u32),

    else => 
        error::wiegt_malformed(name),
}
@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

@petrochenkov

Nobody writes 5 == x in their non-pattern conditions

I am sure I have written that way on many occasions.
In particular, I would write: 5 < x && x < 10 and never x > 5 && x < 10.

The logical AND is called , for some reason, I thought && is usually used for it.

I think the reasons we state in the RFC for rejecting this are valid.
If , is not a liked syntax we should go with @est31's proposal instead in my opinion.

meta is &NameValue(_, Lit::Int(val, ty))

I am not a fan of this syntax. It is inconsistent with the order already used in if let (the reverse). We made a decision way back to have a certain order (pattern first) and I think we should stick with it.

I also think that patterns are more often than not shorter than what they pattern match on, so reversed order would be particularly bad for alignment.

The keyword is also does not feel accurate - matches would be better but that is too long. I'd counter with if meta like &NameValue(_, Lit::Int(val, ty)) but that feels weird too.

I'm assuming that meta is &NameValue(_, Lit::Int(val, ty)) also is an expression on its own that can be used outside of if - is that accurate?

Syntax is the most important aspect in this particular case - the whole point of the change is resyntax nested conditions and conditions with weird inverted order into something nicer looking.

My bad. I could have been clearer - what I meant really is: the most important bit is reducing rightward drift and improving ergonomics - the exact syntax can be changed ;) Updated above.

Contributor

Centril commented Dec 24, 2017

@petrochenkov

Nobody writes 5 == x in their non-pattern conditions

I am sure I have written that way on many occasions.
In particular, I would write: 5 < x && x < 10 and never x > 5 && x < 10.

The logical AND is called , for some reason, I thought && is usually used for it.

I think the reasons we state in the RFC for rejecting this are valid.
If , is not a liked syntax we should go with @est31's proposal instead in my opinion.

meta is &NameValue(_, Lit::Int(val, ty))

I am not a fan of this syntax. It is inconsistent with the order already used in if let (the reverse). We made a decision way back to have a certain order (pattern first) and I think we should stick with it.

I also think that patterns are more often than not shorter than what they pattern match on, so reversed order would be particularly bad for alignment.

The keyword is also does not feel accurate - matches would be better but that is too long. I'd counter with if meta like &NameValue(_, Lit::Int(val, ty)) but that feels weird too.

I'm assuming that meta is &NameValue(_, Lit::Int(val, ty)) also is an expression on its own that can be used outside of if - is that accurate?

Syntax is the most important aspect in this particular case - the whole point of the change is resyntax nested conditions and conditions with weird inverted order into something nicer looking.

My bad. I could have been clearer - what I meant really is: the most important bit is reducing rightward drift and improving ergonomics - the exact syntax can be changed ;) Updated above.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

@matklad That's a very nice and interesting solution indeed =)

With slight change:

let name = meta.name();
when {
    set.is_some() =>
        error::set_again(name),

    let &NameValue(_, Lit::Int(val, ty)) = meta,
    val <= u32::MAX as u64,
    is_unsigned(ty) =>
         *set = Some(val as u32),

    else =>
        error::weight_malformed(name),
}

I see a few problems with your and my version of your syntax - hopefully you can address them? :

  • We are essentially deprecating if / else if / else (or we should, or we have an abundance of too many ways to do it)
  • If Rust was going 1.0 today I would probably buy this syntax right off the bat. After 1.0, this feels like radical change - and while I'm quite the revolutionary, I am not in language design and syntax. Should we not be more incremental?
  • You have introduced one level of indent and rightward drift - it is a constant factor and won't indent further, but it is one thing that makes me reluctant to use match today.
Contributor

Centril commented Dec 24, 2017

@matklad That's a very nice and interesting solution indeed =)

With slight change:

let name = meta.name();
when {
    set.is_some() =>
        error::set_again(name),

    let &NameValue(_, Lit::Int(val, ty)) = meta,
    val <= u32::MAX as u64,
    is_unsigned(ty) =>
         *set = Some(val as u32),

    else =>
        error::weight_malformed(name),
}

I see a few problems with your and my version of your syntax - hopefully you can address them? :

  • We are essentially deprecating if / else if / else (or we should, or we have an abundance of too many ways to do it)
  • If Rust was going 1.0 today I would probably buy this syntax right off the bat. After 1.0, this feels like radical change - and while I'm quite the revolutionary, I am not in language design and syntax. Should we not be more incremental?
  • You have introduced one level of indent and rightward drift - it is a constant factor and won't indent further, but it is one thing that makes me reluctant to use match today.
@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

For comparison:

    if expr.node is ExprAddrOf(_, ref expr)
    && expr.node is ExprArray(ref exprs)
    && exprs.last() is Some(expr)
    && expr.node is ExprLit(ref lit)
    && lit.node is LitKind::Str(ref lit, _) {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
    if let ExprAddrOf(_, ref expr) = expr.node
     , let ExprArray(ref exprs) = expr.node
     , let Some(expr) = exprs.last()
     , let ExprLit(ref lit) = expr.node
     , let LitKind::Str(ref lit, _) = lit.node {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
Contributor

Centril commented Dec 24, 2017

For comparison:

    if expr.node is ExprAddrOf(_, ref expr)
    && expr.node is ExprArray(ref exprs)
    && exprs.last() is Some(expr)
    && expr.node is ExprLit(ref lit)
    && lit.node is LitKind::Str(ref lit, _) {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
    if let ExprAddrOf(_, ref expr) = expr.node
     , let ExprArray(ref exprs) = expr.node
     , let Some(expr) = exprs.last()
     , let ExprLit(ref lit) = expr.node
     , let LitKind::Str(ref lit, _) = lit.node {
        Some((lit.as_str(), exprs.len()))
    } else {
        None
    }
@matklad

This comment has been minimized.

Show comment
Hide comment
@matklad

matklad Dec 24, 2017

Member

@Centril I don't really want to derail this RFC thread into discussing cond, I just want to make sure it's on the radar when we discuss pattern matching improvements :)

That said

  1. cond is important because programmers make errors in if conditions, so simple improvements in this area are great.

  2. I won't be too worried about orthogonality here: "one way to do it" is not a design principle of the Rust language, and the logic for conditions is already not quite orthogonal.

  3. indentation and in general formatting are indeed a problem here. It's fun that in Kotlin, which have both if/else and when, some people use pattern matching on boolean just because it looks neater:

val x = if (cond) 
    bar
else
    baz

vs

val x = when {
  cond -> bar
  else -> baz
}
Member

matklad commented Dec 24, 2017

@Centril I don't really want to derail this RFC thread into discussing cond, I just want to make sure it's on the radar when we discuss pattern matching improvements :)

That said

  1. cond is important because programmers make errors in if conditions, so simple improvements in this area are great.

  2. I won't be too worried about orthogonality here: "one way to do it" is not a design principle of the Rust language, and the logic for conditions is already not quite orthogonal.

  3. indentation and in general formatting are indeed a problem here. It's fun that in Kotlin, which have both if/else and when, some people use pattern matching on boolean just because it looks neater:

val x = if (cond) 
    bar
else
    baz

vs

val x = when {
  cond -> bar
  else -> baz
}
@petrochenkov

This comment has been minimized.

Show comment
Hide comment
@petrochenkov

petrochenkov Dec 24, 2017

Contributor

@Centril

In particular, I would write: 5 < x && x < 10 and never x > 5 && x < 10.

Yeah, I'd probably write this too with less/greater and some other operators, but pattern matching is == (maybe != in the future). I haven't seen people using == like this since very old times before C compilers started issuing basic warnings.

I think the reasons we state in the RFC for rejecting this are valid.

Using && in the syntax leads the the obvious "well, what about ||?"
It also causes confusion with && being part of the expression.
The syntax gives the impression that let is now an expression, which it isn't.

It is very much desirable for non-exhaustive pattern matching construction to be an expression, so it's not confusion, it's correct understanding. (Also, the EXPR is PATTERN expression syntax can be added to the language backward compatibly (with same priority as EXPR as TYPE), so it's not a problem technically).
Since EXPR is PATTERN is a usual boolean expression, || can be naturally supported as well (precise scoping rules for bindings need to be worked out though).

I am not a fan of this syntax. It is inconsistent with the order already used in if let (the reverse). We made a decision way back to have a certain order (pattern first) and I think we should stick with it.

It's the order in if let that's inconsistent - match uses expression on the left and patterns on the right.
Swapping the order when rewriting match as if let and vice versa is one of primary if let annoyances that is is supposed to fix.
If is is implemented, I'd be happy to never see if let/while let in my code again.

I also think that patterns are more often than not shorter than what they pattern match on, so reversed order would be particularly bad for alignment.

Can't say much about alignment. rustfmt have standard rules for ifs and conditions and I'm okay with them.

The keyword is also does not feel accurate - matches would be better but that is too long.

Let's look at these function for example https://github.com/rust-lang/rust/blob/master/src/librustc/ty/sty.rs#L1263-L1518
They are used to shorten non-exhaustive match expressions that we are discussing and start with is_.
fn is_some() and fn is_none()on Option aren't called matches_some/matches_none as well.
It seems natural to turns this from naming convention into an actual operator (x.is_some() -> x is Some(..)).
Oh, and the syntax is borrowed from C# pattern matching, so at least I'm not alone in thinking it's appropriate.

I'm assuming that meta is &NameValue(_, Lit::Int(val, ty)) also is an expression on its own that can be used outside of if - is that accurate?

Yes, expr is pat is a normal boolean expression that can be used in any contexts (so if let pat = expr { true } else { false } is purged from existence forever.). That's one of the Three Pillars of the feature in addition to composability and order :)
(Again, precise scoping rules for bindings still need to be worked out. I never submitted this as an RFC because I wanted to make a PoC implementation first, but never had enough time).

Contributor

petrochenkov commented Dec 24, 2017

@Centril

In particular, I would write: 5 < x && x < 10 and never x > 5 && x < 10.

Yeah, I'd probably write this too with less/greater and some other operators, but pattern matching is == (maybe != in the future). I haven't seen people using == like this since very old times before C compilers started issuing basic warnings.

I think the reasons we state in the RFC for rejecting this are valid.

Using && in the syntax leads the the obvious "well, what about ||?"
It also causes confusion with && being part of the expression.
The syntax gives the impression that let is now an expression, which it isn't.

It is very much desirable for non-exhaustive pattern matching construction to be an expression, so it's not confusion, it's correct understanding. (Also, the EXPR is PATTERN expression syntax can be added to the language backward compatibly (with same priority as EXPR as TYPE), so it's not a problem technically).
Since EXPR is PATTERN is a usual boolean expression, || can be naturally supported as well (precise scoping rules for bindings need to be worked out though).

I am not a fan of this syntax. It is inconsistent with the order already used in if let (the reverse). We made a decision way back to have a certain order (pattern first) and I think we should stick with it.

It's the order in if let that's inconsistent - match uses expression on the left and patterns on the right.
Swapping the order when rewriting match as if let and vice versa is one of primary if let annoyances that is is supposed to fix.
If is is implemented, I'd be happy to never see if let/while let in my code again.

I also think that patterns are more often than not shorter than what they pattern match on, so reversed order would be particularly bad for alignment.

Can't say much about alignment. rustfmt have standard rules for ifs and conditions and I'm okay with them.

The keyword is also does not feel accurate - matches would be better but that is too long.

Let's look at these function for example https://github.com/rust-lang/rust/blob/master/src/librustc/ty/sty.rs#L1263-L1518
They are used to shorten non-exhaustive match expressions that we are discussing and start with is_.
fn is_some() and fn is_none()on Option aren't called matches_some/matches_none as well.
It seems natural to turns this from naming convention into an actual operator (x.is_some() -> x is Some(..)).
Oh, and the syntax is borrowed from C# pattern matching, so at least I'm not alone in thinking it's appropriate.

I'm assuming that meta is &NameValue(_, Lit::Int(val, ty)) also is an expression on its own that can be used outside of if - is that accurate?

Yes, expr is pat is a normal boolean expression that can be used in any contexts (so if let pat = expr { true } else { false } is purged from existence forever.). That's one of the Three Pillars of the feature in addition to composability and order :)
(Again, precise scoping rules for bindings still need to be worked out. I never submitted this as an RFC because I wanted to make a PoC implementation first, but never had enough time).

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

@matklad I won't be too worried about orthogonality here: "one way to do it" is not a design principle of the Rust language, and the logic for conditions is already not quite orthogonal.

You are not derailing, it was an interesting proposal.

There's having one way, which I am not a proponent of, and there's having 10 ways, which I am not a proponent of either, and somewhere you have to strike a balance... ;)

@petrochenkov

It's the order in if let that's inconsistent

No matter, it is what we got... so don't we have to be consistent with the inconsistency of if let?

match uses expression on the left and patterns on the right.

match expr_on {
    pat_1 => expr_1,
    ...
}

Here, pat_1 is to the left. If you write match expr_on { pat_1 => expr_1 } you can make the case that pat_1 is on the right, but I don't buy this.

It seems natural to turns this from naming convention into an actual operator (x.is_some() -> x is Some(..)).

Sounds reasonable. You've convinced me on this point =) However, x.is_some() will still have its place.

Contributor

Centril commented Dec 24, 2017

@matklad I won't be too worried about orthogonality here: "one way to do it" is not a design principle of the Rust language, and the logic for conditions is already not quite orthogonal.

You are not derailing, it was an interesting proposal.

There's having one way, which I am not a proponent of, and there's having 10 ways, which I am not a proponent of either, and somewhere you have to strike a balance... ;)

@petrochenkov

It's the order in if let that's inconsistent

No matter, it is what we got... so don't we have to be consistent with the inconsistency of if let?

match uses expression on the left and patterns on the right.

match expr_on {
    pat_1 => expr_1,
    ...
}

Here, pat_1 is to the left. If you write match expr_on { pat_1 => expr_1 } you can make the case that pat_1 is on the right, but I don't buy this.

It seems natural to turns this from naming convention into an actual operator (x.is_some() -> x is Some(..)).

Sounds reasonable. You've convinced me on this point =) However, x.is_some() will still have its place.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

You've all made great points. I don't feel very strongly about the particular syntax proposed in the RFC, so at this point I am certainly willing to rework this RFC, with some help, into either when or EXPR is PATTERN or both. Perhaps then we should do EXPR is PATTERN first and then when later since they seem to be compatible. What do you think about this @scottmcm ?

Contributor

Centril commented Dec 24, 2017

You've all made great points. I don't feel very strongly about the particular syntax proposed in the RFC, so at this point I am certainly willing to rework this RFC, with some help, into either when or EXPR is PATTERN or both. Perhaps then we should do EXPR is PATTERN first and then when later since they seem to be compatible. What do you think about this @scottmcm ?

@mdinger

This comment has been minimized.

Show comment
Hide comment
@mdinger

mdinger Dec 24, 2017

Contributor

Fwiw, as written this rfc seems to follow the ideas in #929 fairly closely and while 'is' was mentioned in that issue, it was relatively recently added in the thread and easy to miss. As such, it isn't surprising it wasn't included already in the rfc as a variant or option.

A positive note of both these is they both appear to make the language consistent and more general which is a huge improvement over the current situation in which 'if let' is considered an eyesore.

Having said that, the greater potential benefit of the 'is' is pretty cool.

Something I didn't see in this rfc is the inclusion of 'where' as seen in the following example taken from #929 (comment) :

if let a = foo(), let b = a.bar(), let c = b.baz() where c > 2, let d = c.quux() {
    // all operations succeeded
} else {
    // something failed
}
Contributor

mdinger commented Dec 24, 2017

Fwiw, as written this rfc seems to follow the ideas in #929 fairly closely and while 'is' was mentioned in that issue, it was relatively recently added in the thread and easy to miss. As such, it isn't surprising it wasn't included already in the rfc as a variant or option.

A positive note of both these is they both appear to make the language consistent and more general which is a huge improvement over the current situation in which 'if let' is considered an eyesore.

Having said that, the greater potential benefit of the 'is' is pretty cool.

Something I didn't see in this rfc is the inclusion of 'where' as seen in the following example taken from #929 (comment) :

if let a = foo(), let b = a.bar(), let c = b.baz() where c > 2, let d = c.quux() {
    // all operations succeeded
} else {
    // something failed
}
@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 24, 2017

Contributor

@mdinger I think we just forgot about it when writing about alternatives because there are so many potential syntactic variants, you have to stop somewhere ;) With respect to where it seems redundant to me, and aligns poorly. Also, I've never considered if let an eyesore - just clunky when you have to match on more.

Contributor

Centril commented Dec 24, 2017

@mdinger I think we just forgot about it when writing about alternatives because there are so many potential syntactic variants, you have to stop somewhere ;) With respect to where it seems redundant to me, and aligns poorly. Also, I've never considered if let an eyesore - just clunky when you have to match on more.

@Havvy

This comment has been minimized.

Show comment
Hide comment
@Havvy

Havvy Dec 25, 2017

Contributor

I'm in favor of chaining if let as per this RFC over an is/matches operator mainly due to the fact that as an expression, is and matches cannot introduce new bindings outside of the expression without being extremely inconsistent with the rest of the language. E.g. a is B(c) && c.is_foo() should be illegal (and then you'd write it as a is B(_) so as to not introduce any bindings). With if let, the binding is a-okay because it's only valid in a subexpression of the main if-let expression.

And sure, you could say that an is expression in the predicate portion of an if expression can introduce new bindings when used with an is expression, but that's really special casing the semantics in a bad way.


The whole pattern = expr thing in if let is also not inconsistent with the language. Let statements are the same way. Function parameters are pattern : type but if we add default arguments, it'd probably be pattern: type = expr or something of the same order. Only match is expr { pattern => expr, ... }.

Contributor

Havvy commented Dec 25, 2017

I'm in favor of chaining if let as per this RFC over an is/matches operator mainly due to the fact that as an expression, is and matches cannot introduce new bindings outside of the expression without being extremely inconsistent with the rest of the language. E.g. a is B(c) && c.is_foo() should be illegal (and then you'd write it as a is B(_) so as to not introduce any bindings). With if let, the binding is a-okay because it's only valid in a subexpression of the main if-let expression.

And sure, you could say that an is expression in the predicate portion of an if expression can introduce new bindings when used with an is expression, but that's really special casing the semantics in a bad way.


The whole pattern = expr thing in if let is also not inconsistent with the language. Let statements are the same way. Function parameters are pattern : type but if we add default arguments, it'd probably be pattern: type = expr or something of the same order. Only match is expr { pattern => expr, ... }.

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Dec 25, 2017

Member

My problem with is-that-can-bind is that whether the binding should be in scope is arbitrarily complicated, up to incomputable. If I take the example from there:

if opt_x is Some(x) && x > 10 {
    println!("{}", x);
}

there are just so many tiny changes that make it no longer work:

if opt_x is Some(x) || x > 10 {
if (b == opt_x is Some(x)) && x > 10 {
if foo(opt_x is Some(x)) && x > 10 {

I'm scared just thinking of how to write a thorough but non-surprising rule here.

Aside: I prefer is to replace all those common methods like .is_some() that don't introduce bindings, so you can x is Some(_) or c is 'a'...'z' or x is Err(ErrorKind::FooBar) (without ==).

I think I'm personally coming around to @est31's "if guards on ifs". I like that it would extend to match naturally, if you think of if as a pattern suffix and just keep adding more and more if or if let suffixes. So then the if expression is just a single if guard, in a sense, with special else support.

Member

scottmcm commented Dec 25, 2017

My problem with is-that-can-bind is that whether the binding should be in scope is arbitrarily complicated, up to incomputable. If I take the example from there:

if opt_x is Some(x) && x > 10 {
    println!("{}", x);
}

there are just so many tiny changes that make it no longer work:

if opt_x is Some(x) || x > 10 {
if (b == opt_x is Some(x)) && x > 10 {
if foo(opt_x is Some(x)) && x > 10 {

I'm scared just thinking of how to write a thorough but non-surprising rule here.

Aside: I prefer is to replace all those common methods like .is_some() that don't introduce bindings, so you can x is Some(_) or c is 'a'...'z' or x is Err(ErrorKind::FooBar) (without ==).

I think I'm personally coming around to @est31's "if guards on ifs". I like that it would extend to match naturally, if you think of if as a pattern suffix and just keep adding more and more if or if let suffixes. So then the if expression is just a single if guard, in a sense, with special else support.

@matklad

This comment has been minimized.

Show comment
Hide comment
@matklad

matklad Dec 25, 2017

Member

My problem with is-that-can-bind is that whether the binding should be in scope is arbitrarily complicated, up to incomputable.

It's interesting to compare it with Kotlin, which also uses is operator for the similar purpose: https://kotlinlang.org/docs/reference/typecasts.html#smart-casts.

The differences is that instead of destructing, Kotlin's is supplies a flow-sensitive type information. The compiler indeed uses pretty smart control-flow analysis to check if every use of a variable is dominated by the is check.

However, as long as the compiler does all the inference work for you, actually using this feature is easy: you don't have to replay the analysis in your head when reading or writing code, because the compiler catches all errors.

Member

matklad commented Dec 25, 2017

My problem with is-that-can-bind is that whether the binding should be in scope is arbitrarily complicated, up to incomputable.

It's interesting to compare it with Kotlin, which also uses is operator for the similar purpose: https://kotlinlang.org/docs/reference/typecasts.html#smart-casts.

The differences is that instead of destructing, Kotlin's is supplies a flow-sensitive type information. The compiler indeed uses pretty smart control-flow analysis to check if every use of a variable is dominated by the is check.

However, as long as the compiler does all the inference work for you, actually using this feature is easy: you don't have to replay the analysis in your head when reading or writing code, because the compiler catches all errors.

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Dec 25, 2017

Contributor

I think is-that-can't-bind is, while it would require a new (at least contextual) keyword, a great addition. Not so sure about is-that-can-bind, due to the weird behaviour that this would create (see @scottmcm 's comment above).

Contributor

est31 commented Dec 25, 2017

I think is-that-can't-bind is, while it would require a new (at least contextual) keyword, a great addition. Not so sure about is-that-can-bind, due to the weird behaviour that this would create (see @scottmcm 's comment above).

@matklad

This comment has been minimized.

Show comment
Hide comment
@matklad

matklad Dec 25, 2017

Member

due to the weird behaviour that this would create

I disagree that this behavior should be called weird, this is essentially control-flow sensitive typechecking, which is more or less mainstream today, and can be found in C#, Kotlin and TypeScript.

Member

matklad commented Dec 25, 2017

due to the weird behaviour that this would create

I disagree that this behavior should be called weird, this is essentially control-flow sensitive typechecking, which is more or less mainstream today, and can be found in C#, Kotlin and TypeScript.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Dec 25, 2017

Contributor

@matklad Do you have some relevant papers I could read on this topic perhaps? Might be useful when rewriting the RFC if we decide to go in this direction.

PS: You have some private messages on IRC ;)

Contributor

Centril commented Dec 25, 2017

@matklad Do you have some relevant papers I could read on this topic perhaps? Might be useful when rewriting the RFC if we decide to go in this direction.

PS: You have some private messages on IRC ;)

@rkruppe

This comment has been minimized.

Show comment
Hide comment
@rkruppe

rkruppe Dec 25, 2017

Contributor

As far as I can see, C#, Kotlin and TypeScript (and other languages I've heard of) all have only flow-sensitive type "refinement", rather than type-dependent bindings. The typing judgements may look similar or identical for both, but the implementation in a compiler will be different (name resolution is affected, not just type checking) and the UX may be rather different as well. It certainly feels different to me, and I'm perfectly comfortable with the idea of flow-sensitive typing.

Contributor

rkruppe commented Dec 25, 2017

As far as I can see, C#, Kotlin and TypeScript (and other languages I've heard of) all have only flow-sensitive type "refinement", rather than type-dependent bindings. The typing judgements may look similar or identical for both, but the implementation in a compiler will be different (name resolution is affected, not just type checking) and the UX may be rather different as well. It certainly feels different to me, and I'm perfectly comfortable with the idea of flow-sensitive typing.

@stepancheg

This comment has been minimized.

Show comment
Hide comment
@stepancheg

stepancheg Jan 3, 2018

@petrochenkov @matklad for the record, same parser ambuity issue issue exists without is. E. g. this does not compile:

#[derive(PartialEq,Eq)]
struct Foo {}

fn main() {
    let f = Foo {};
    if f == Foo {} {
    }
}

or this:

struct Foo {}

impl Foo {
    fn bar(&self) -> bool { false }
}

fn main() {
    if Foo {}.bar() {
    }
}

I still think is expression with parens (Foo {}) is better than if let, but I understand people who think it's ugly.

stepancheg commented Jan 3, 2018

@petrochenkov @matklad for the record, same parser ambuity issue issue exists without is. E. g. this does not compile:

#[derive(PartialEq,Eq)]
struct Foo {}

fn main() {
    let f = Foo {};
    if f == Foo {} {
    }
}

or this:

struct Foo {}

impl Foo {
    fn bar(&self) -> bool { false }
}

fn main() {
    if Foo {}.bar() {
    }
}

I still think is expression with parens (Foo {}) is better than if let, but I understand people who think it's ugly.

@I60R

This comment has been minimized.

Show comment
Hide comment
@I60R

I60R Jan 5, 2018

@petrochenkov
thank you for clarifying that.

At least it solves:

  1. Backwards compatibility
  2. Implementing most of the extensions mentioned ITT
  3. Simplicity to not become a mess and not to introduce new problems

Yes, it don't returns bool.
But what is the problem to implement is (that don't introduces bindings) alongside?
That seems to be the most optimal way

I60R commented Jan 5, 2018

@petrochenkov
thank you for clarifying that.

At least it solves:

  1. Backwards compatibility
  2. Implementing most of the extensions mentioned ITT
  3. Simplicity to not become a mess and not to introduce new problems

Yes, it don't returns bool.
But what is the problem to implement is (that don't introduces bindings) alongside?
That seems to be the most optimal way

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Jan 6, 2018

Contributor

The survey ran from 2017-12-31 06:25 to ~2018-01-06 ~14:00.

373 people took the survey.

It was distributed to:

  • Reddit, 68.4%
  • internals.rust-lang.org, 16.6%
  • users.rust-lang.org, 7.5%
  • IRC, 5.1%
  • The RFC, 2.4%

Breakdown of preferences (ordered by the answer and not how much it was liked/disliked):

  • Using , and let PATTERN = EXPR - liked: 16.9%, disliked: 56.3%
  • Using && and let PATTERN = EXPR - liked: 66.2%, disliked: 16.9%
  • Using if and let PATTERN = EXPR - liked: 12.3%, disliked: 66%
  • Using , and EXPR is PATTERN - liked: 4.3%, disliked: 74.5%
  • Using && and EXPR is PATTERN - liked: 24.9%, disliked: 48.5%
  • Using if and EXPR is PATTERN - liked: 2.4%, disliked: 80.4%
  • None of them - liked: 9.7%, disliked: 1.9%

Note: "none of them" being disliked means that the person dislikes none of the 6 preceding alternatives.

To decrease the risk of bias, the order of the answers were randomized.

A summary of the survey: https://docs.google.com/forms/d/e/1FAIpQLScwG0Y3ynA9aJZ-iprOey_GyCNeFMO9MSDJR1kiskpjsjL1Mw/viewanalytics

A CVS file of the survey: https://drive.google.com/file/d/1awyvryblSHFH9J77TPutW5BrRlKr0EKZ/view?usp=sharing

A PDF for the survey: https://drive.google.com/file/d/14ofF5on_Z_XLvhPr1I4dVgCfcybQO2GY/view?usp=sharing

Contributor

Centril commented Jan 6, 2018

The survey ran from 2017-12-31 06:25 to ~2018-01-06 ~14:00.

373 people took the survey.

It was distributed to:

  • Reddit, 68.4%
  • internals.rust-lang.org, 16.6%
  • users.rust-lang.org, 7.5%
  • IRC, 5.1%
  • The RFC, 2.4%

Breakdown of preferences (ordered by the answer and not how much it was liked/disliked):

  • Using , and let PATTERN = EXPR - liked: 16.9%, disliked: 56.3%
  • Using && and let PATTERN = EXPR - liked: 66.2%, disliked: 16.9%
  • Using if and let PATTERN = EXPR - liked: 12.3%, disliked: 66%
  • Using , and EXPR is PATTERN - liked: 4.3%, disliked: 74.5%
  • Using && and EXPR is PATTERN - liked: 24.9%, disliked: 48.5%
  • Using if and EXPR is PATTERN - liked: 2.4%, disliked: 80.4%
  • None of them - liked: 9.7%, disliked: 1.9%

Note: "none of them" being disliked means that the person dislikes none of the 6 preceding alternatives.

To decrease the risk of bias, the order of the answers were randomized.

A summary of the survey: https://docs.google.com/forms/d/e/1FAIpQLScwG0Y3ynA9aJZ-iprOey_GyCNeFMO9MSDJR1kiskpjsjL1Mw/viewanalytics

A CVS file of the survey: https://drive.google.com/file/d/1awyvryblSHFH9J77TPutW5BrRlKr0EKZ/view?usp=sharing

A PDF for the survey: https://drive.google.com/file/d/14ofF5on_Z_XLvhPr1I4dVgCfcybQO2GY/view?usp=sharing

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Jan 6, 2018

Contributor

@Centril thanks for running the survey! While having a general "what looks best" poll is great, I think the most valuable part of it were the comments. They contain a bunch of very interesting arguments and most of them were actually on-topic unlike most of the comments inside this thread :p.

Contributor

est31 commented Jan 6, 2018

@Centril thanks for running the survey! While having a general "what looks best" poll is great, I think the most valuable part of it were the comments. They contain a bunch of very interesting arguments and most of them were actually on-topic unlike most of the comments inside this thread :p.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Jan 6, 2018

Contributor

Indeed the comments are very valuable and they show that the choices people have made are not made on a whim but that the answers are well reasoned. Thanks a lot to @panicbit for helping out with making the survey.

Contributor

Centril commented Jan 6, 2018

Indeed the comments are very valuable and they show that the choices people have made are not made on a whim but that the answers are well reasoned. Thanks a lot to @panicbit for helping out with making the survey.

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Jan 6, 2018

Contributor

@Centril I hope you won't just blindly take the results of this poll and go with whatever was most popular? This was never what the poll was about.

Contributor

est31 commented Jan 6, 2018

@Centril I hope you won't just blindly take the results of this poll and go with whatever was most popular? This was never what the poll was about.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Jan 6, 2018

Contributor

Certainly not blindly. I would put it this way: Other proposals now have a somewhat increased burden of proof to show why the reasons put forth by the majority, such as "this is more consistent with current Rust" are less important than the reasons put forth by the minority positions. The reasons for picking the majority position as well as not picking the minority ones seem well argued by the respondents.

Contributor

Centril commented Jan 6, 2018

Certainly not blindly. I would put it this way: Other proposals now have a somewhat increased burden of proof to show why the reasons put forth by the majority, such as "this is more consistent with current Rust" are less important than the reasons put forth by the minority positions. The reasons for picking the majority position as well as not picking the minority ones seem well argued by the respondents.

@est31

This comment has been minimized.

Show comment
Hide comment
@est31

est31 Jan 6, 2018

Contributor

The poll was made in a way where no sum up of the arguments and the downsides and upsides and hidden gotchas of the various proposals was provided. This means that the people filling out the questions weren't able to make an informed descision unless they thought about those themselves, and I'm sure that most just answered basing on their gut feeling. Even if you provide the information, many people might not read it, so basing decisions on polls is no good solution.

The proposal provided very interesting arguments like e.g. saying that , might confuse C coders because , here means execution where the result is discarded, and (quoting an argument against my proposal) if guards might confuse C coders as well because they might think now that in Rust, if doesn't need any braces while in fact it does. Another comment brought up if let a = b && c parsing ambiguities. How many people knew about all of these arguments?

Also, note that the least disliked option is to do nothing.

Either way, there hasn't been a single involvement by the lang team on this. What are their opinions on the matter? Which option is best for them?

Contributor

est31 commented Jan 6, 2018

The poll was made in a way where no sum up of the arguments and the downsides and upsides and hidden gotchas of the various proposals was provided. This means that the people filling out the questions weren't able to make an informed descision unless they thought about those themselves, and I'm sure that most just answered basing on their gut feeling. Even if you provide the information, many people might not read it, so basing decisions on polls is no good solution.

The proposal provided very interesting arguments like e.g. saying that , might confuse C coders because , here means execution where the result is discarded, and (quoting an argument against my proposal) if guards might confuse C coders as well because they might think now that in Rust, if doesn't need any braces while in fact it does. Another comment brought up if let a = b && c parsing ambiguities. How many people knew about all of these arguments?

Also, note that the least disliked option is to do nothing.

Either way, there hasn't been a single involvement by the lang team on this. What are their opinions on the matter? Which option is best for them?

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Jan 6, 2018

Contributor

I've read at least 300 of the answers, and we can cherry pick answers, but my feeling is that a lot of them were informed and good comments.

Also, note that the least disliked option is to do nothing.

That's a misinterpretation of the meaning of disliking none of them. Disliking none of them means that you can live with any of the 6 syntaxes proposed. At least that is what the question/answer intended.

Contributor

Centril commented Jan 6, 2018

I've read at least 300 of the answers, and we can cherry pick answers, but my feeling is that a lot of them were informed and good comments.

Also, note that the least disliked option is to do nothing.

That's a misinterpretation of the meaning of disliking none of them. Disliking none of them means that you can live with any of the 6 syntaxes proposed. At least that is what the question/answer intended.

@scottmcm

This comment has been minimized.

Show comment
Hide comment
@scottmcm

scottmcm Jan 6, 2018

Member

Reading through the summary, this comment (I didn't look from whom) stood out to me:

In the end I'm very torn on whether any of this is "good enough". All of the proposals seem to come with unintuitive caveats - either the syntax feels unintuitive or the implications are unintuitive.

I think my wish right now would be to

  1. Postpone this for now,
  2. Accept #2046 so macros can have better codegen while experimenting with what the syntax should be for this, and
  3. Revisit the problem space of #1303, as most of my chaining uses would be adequately handled by that.
Member

scottmcm commented Jan 6, 2018

Reading through the summary, this comment (I didn't look from whom) stood out to me:

In the end I'm very torn on whether any of this is "good enough". All of the proposals seem to come with unintuitive caveats - either the syntax feels unintuitive or the implications are unintuitive.

I think my wish right now would be to

  1. Postpone this for now,
  2. Accept #2046 so macros can have better codegen while experimenting with what the syntax should be for this, and
  3. Revisit the problem space of #1303, as most of my chaining uses would be adequately handled by that.
@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Jan 6, 2018

Contributor

As this RFC is written today, it is wholly insufficient to cover alternatives and discuss the pros and cons. The survey has also highlighted may arguments for and against each alternative. The guide-level-explanation might also be changed to use a different syntax. Furthermore, this RFC has had >= 150 comments, so changes to the proposal may be highly confusing for a reader.

Given all this, I think the best course of action is to close the RFC. I will be doing so in a day unless there are major objections from folks.

Contributor

Centril commented Jan 6, 2018

As this RFC is written today, it is wholly insufficient to cover alternatives and discuss the pros and cons. The survey has also highlighted may arguments for and against each alternative. The guide-level-explanation might also be changed to use a different syntax. Furthermore, this RFC has had >= 150 comments, so changes to the proposal may be highly confusing for a reader.

Given all this, I think the best course of action is to close the RFC. I will be doing so in a day unless there are major objections from folks.

@petrochenkov

This comment has been minimized.

Show comment
Hide comment
@petrochenkov

petrochenkov Jan 6, 2018

Contributor

I'm preparing a prototype for in-expression bindings right now, btw.
But I guess it won't disappear anywhere if the RFC is closed.


Many (EDIT: but not all!) comments against EXPR is PAT can be summarized as "New keyword. Scary. Unnecessary.".
Since more popular let PAT = EXPR cannot be reused for technical reasons (at least not without a bunch of compatibility hacks), I wonder if EXPR match PAT can address these concerns. It reuses an existing non-scary and necessary keyword, can be taught as a short "inline" form of full match, and the operand order is consistent with full match as well.

Contributor

petrochenkov commented Jan 6, 2018

I'm preparing a prototype for in-expression bindings right now, btw.
But I guess it won't disappear anywhere if the RFC is closed.


Many (EDIT: but not all!) comments against EXPR is PAT can be summarized as "New keyword. Scary. Unnecessary.".
Since more popular let PAT = EXPR cannot be reused for technical reasons (at least not without a bunch of compatibility hacks), I wonder if EXPR match PAT can address these concerns. It reuses an existing non-scary and necessary keyword, can be taught as a short "inline" form of full match, and the operand order is consistent with full match as well.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Jan 6, 2018

Contributor

@petrochenkov The keyword match makes me much more comfortable. It addresses among other things:

  • Not confusing python users with is.
  • Not confusing readers with as.
  • It is semantically clearer and probably more correct since an is that introduces bindings is weird.

My only nit is that match has the wrong tense, but I can certainly live with that. Using matches would be too long, decrease familiarity, and introduce a new keyword.

Contributor

Centril commented Jan 6, 2018

@petrochenkov The keyword match makes me much more comfortable. It addresses among other things:

  • Not confusing python users with is.
  • Not confusing readers with as.
  • It is semantically clearer and probably more correct since an is that introduces bindings is weird.

My only nit is that match has the wrong tense, but I can certainly live with that. Using matches would be too long, decrease familiarity, and introduce a new keyword.

@rpjohnst

This comment has been minimized.

Show comment
Hide comment
@rpjohnst

rpjohnst Jan 6, 2018

I do prefer EXPR match PAT to the is version, but I don't think the reaction can be fairly summarized as simply "new. scary. unnecessary." Introducing bindings mid-expression is a big expansion.

I also agree with @scottmcm that let..else would cover the vast majority of my use cases here. It also matches my preferred style- it makes it much easier to split up and name the parts of large conditions, rather than encouraging they all be combined into one giant if let.

rpjohnst commented Jan 6, 2018

I do prefer EXPR match PAT to the is version, but I don't think the reaction can be fairly summarized as simply "new. scary. unnecessary." Introducing bindings mid-expression is a big expansion.

I also agree with @scottmcm that let..else would cover the vast majority of my use cases here. It also matches my preferred style- it makes it much easier to split up and name the parts of large conditions, rather than encouraging they all be combined into one giant if let.

@petrochenkov

This comment has been minimized.

Show comment
Hide comment
@petrochenkov

petrochenkov Jan 6, 2018

Contributor

@rpjohnst

I don't think the reaction can be fairly summarized as simply "new. scary. unnecessary."

This is just bad wording on my side, I didn't mean it's a summary for all reactions, just the most popular concern.
Concerns about in-expression bindings in general are not in this category.

Contributor

petrochenkov commented Jan 6, 2018

@rpjohnst

I don't think the reaction can be fairly summarized as simply "new. scary. unnecessary."

This is just bad wording on my side, I didn't mean it's a summary for all reactions, just the most popular concern.
Concerns about in-expression bindings in general are not in this category.

@Centril

This comment has been minimized.

Show comment
Hide comment
@Centril

Centril Jan 8, 2018

Contributor

Closing as promised.

Contributor

Centril commented Jan 8, 2018

Closing as promised.

@petrochenkov

This comment has been minimized.

Show comment
Hide comment
@petrochenkov

petrochenkov Feb 20, 2018

Contributor

So, I finally completed my prototype of non-exhaustive pattern-matching expressions, including in-expression bindings.
The implementation is available on my branch: https://github.com/petrochenkov/rust/tree/isproto.
I also ported rustfmt to use is instead of if let, while let and sometimes match.
Rustfmt branch is available here - https://github.com/petrochenkov/rustfmt/tree/isproto.

Some notes:

  • I used EXPR is PAT (or rather EXPR is PAT1 | PAT2 | ...) as syntax because I believe this is the right syntax in general, but also because it's legacy-free and I could freely tweak scopes and lifetimes of introduced bindings while successfully compiling existing code not using the feature.
  • The feature is implemented as a transformation on AST and resolution tables. Lowering to MIR may be a better place, but I'm just familiar with front-end better.
  • Desugaring heavily relies on labeled loops and labeled breaks with values (see comments in desugar_is.rs).
  • Limitations: is doesn't work in constant expressions due to limitations of const evaluator (should be fixed by #2342 and #2341), is with bindings doesn't work in if guards in match due to lack of break 'arm construction (see #2294 (comment)).
  • Two variants of binding scopes are implemented - the longer one (containing block), by default, and the shorter one (current statement/full expression) enabled by a crate-level attribute.
  • The "longer" variant allows to refer to is bindings after the statement they were introduced in (similarly to plain let), but they are still considered potentially uninitialized.
    For them to be considered initialized we need to incorporate pattern exhaustiveness into initialization checking. Basically, this should work:
    let x;
    match Some(10) {
        Some(..) | None => {
            x = 11;
        }
        _ => {} // Unreachable
    }
    let y = x; // Currently ERROR: use of possibly uninitialized variable: `x`
  • The "longer" variant can result in surprising and undesired shadowing and overlong lifetimes compared to the "short" variant (see commit switching to the short variant and code "fixed" by it here - petrochenkov/rustfmt@f218480).
  • The "short" variant still can result in overlong lifetimes compared to existing if let/while let (without NLL), examples of such overlong lifetimes are marked with "ISPROTO" comments in my rustfmt branch.
    I tried to build rustfmt with feature(nll) to find out whether the borrowing errors disappear with it, but hit an ICE (rust-lang/rust#48132).
    Update: the borrowing issue in minified form looks like get_temporary() is Pattern(ref binding) and it's indeed fixed by NLL!
  • Overlong lifetimes in while epxr is pat { ... } are especially funny because bindings always have to be mut - they live for the whole loop and are assigned in every loop cycle!
    Bindings introduced in while let notably live for a single cycle so they don't have to be mut. So, it may be reasonable to reduce scopes/lifetimes of is bindings to something even shorter than full expressions, but full expressions are a viable conservative variant.
  • I have found no examples of the struct literal parsing issue (#2260 (comment)) in rustfmt.
Contributor

petrochenkov commented Feb 20, 2018

So, I finally completed my prototype of non-exhaustive pattern-matching expressions, including in-expression bindings.
The implementation is available on my branch: https://github.com/petrochenkov/rust/tree/isproto.
I also ported rustfmt to use is instead of if let, while let and sometimes match.
Rustfmt branch is available here - https://github.com/petrochenkov/rustfmt/tree/isproto.

Some notes:

  • I used EXPR is PAT (or rather EXPR is PAT1 | PAT2 | ...) as syntax because I believe this is the right syntax in general, but also because it's legacy-free and I could freely tweak scopes and lifetimes of introduced bindings while successfully compiling existing code not using the feature.
  • The feature is implemented as a transformation on AST and resolution tables. Lowering to MIR may be a better place, but I'm just familiar with front-end better.
  • Desugaring heavily relies on labeled loops and labeled breaks with values (see comments in desugar_is.rs).
  • Limitations: is doesn't work in constant expressions due to limitations of const evaluator (should be fixed by #2342 and #2341), is with bindings doesn't work in if guards in match due to lack of break 'arm construction (see #2294 (comment)).
  • Two variants of binding scopes are implemented - the longer one (containing block), by default, and the shorter one (current statement/full expression) enabled by a crate-level attribute.
  • The "longer" variant allows to refer to is bindings after the statement they were introduced in (similarly to plain let), but they are still considered potentially uninitialized.
    For them to be considered initialized we need to incorporate pattern exhaustiveness into initialization checking. Basically, this should work:
    let x;
    match Some(10) {
        Some(..) | None => {
            x = 11;
        }
        _ => {} // Unreachable
    }
    let y = x; // Currently ERROR: use of possibly uninitialized variable: `x`
  • The "longer" variant can result in surprising and undesired shadowing and overlong lifetimes compared to the "short" variant (see commit switching to the short variant and code "fixed" by it here - petrochenkov/rustfmt@f218480).
  • The "short" variant still can result in overlong lifetimes compared to existing if let/while let (without NLL), examples of such overlong lifetimes are marked with "ISPROTO" comments in my rustfmt branch.
    I tried to build rustfmt with feature(nll) to find out whether the borrowing errors disappear with it, but hit an ICE (rust-lang/rust#48132).
    Update: the borrowing issue in minified form looks like get_temporary() is Pattern(ref binding) and it's indeed fixed by NLL!
  • Overlong lifetimes in while epxr is pat { ... } are especially funny because bindings always have to be mut - they live for the whole loop and are assigned in every loop cycle!
    Bindings introduced in while let notably live for a single cycle so they don't have to be mut. So, it may be reasonable to reduce scopes/lifetimes of is bindings to something even shorter than full expressions, but full expressions are a viable conservative variant.
  • I have found no examples of the struct literal parsing issue (#2260 (comment)) in rustfmt.
@I60R

This comment has been minimized.

Show comment
Hide comment
@I60R

I60R Jul 3, 2018

My suggestion about || scoping rules:

  1. Bindings introduced on both || sides lives in different scopes
    This means that bindings introduced on left || side are not visible on right.
    (x is Ok(y) || y.condition()) ⇒ error (unless y is not defined in parent scope)
  2. Binding always should be "mirrored" on other || side to be visible after condition
    This will provide guarantee that bindings introduced in all conditions can be always accessed safely after && operator and in if→then expression.
    Also in this way it will be easier to think about scopes and shadowing.
    (x is Ok(y) || condition()) ⇒ error, y must be defined on other || side
    (x is Ok(y) || z is Ok(y)) && y.condition()) ⇒ valid
  3. If binding is required only in one || side scope, it should be explicitly declared as "local"
    This will relax second rule when required (I'm sure, there will be use cases).
    Special syntax will determine scope where binding is visible:
    // suggestion 1
    if ('l: x is Ok('l y) && y.condition()) || condition() { 
    }
    // suggestion 2
    if x is Ok('l y) && y.condition() 'l || condition() {
    }
    // suggestion 3
    if x is Ok(priv y) && y.condition() || condition() {
    }
    // there might be other syntax as well

I60R commented Jul 3, 2018

My suggestion about || scoping rules:

  1. Bindings introduced on both || sides lives in different scopes
    This means that bindings introduced on left || side are not visible on right.
    (x is Ok(y) || y.condition()) ⇒ error (unless y is not defined in parent scope)
  2. Binding always should be "mirrored" on other || side to be visible after condition
    This will provide guarantee that bindings introduced in all conditions can be always accessed safely after && operator and in if→then expression.
    Also in this way it will be easier to think about scopes and shadowing.
    (x is Ok(y) || condition()) ⇒ error, y must be defined on other || side
    (x is Ok(y) || z is Ok(y)) && y.condition()) ⇒ valid
  3. If binding is required only in one || side scope, it should be explicitly declared as "local"
    This will relax second rule when required (I'm sure, there will be use cases).
    Special syntax will determine scope where binding is visible:
    // suggestion 1
    if ('l: x is Ok('l y) && y.condition()) || condition() { 
    }
    // suggestion 2
    if x is Ok('l y) && y.condition() 'l || condition() {
    }
    // suggestion 3
    if x is Ok(priv y) && y.condition() || condition() {
    }
    // there might be other syntax as well
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment