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: Local `loop` bindings #2617

Open
wants to merge 9 commits into
base: master
from

Conversation

Projects
None yet
@porky11
Copy link

porky11 commented Dec 25, 2018

This RFC is about adding local loop bindings.

Rendered

Here a simple example for the new syntax:

fn factorial(x: i32) -> i32 {
    loop (result, count) = (1, x) {
        if count == 1 {
            break result;
        }
        (result * count, count - 1)
    }
}
Create 0000-local-loop-bindings.md
This RFC is about adding local loop bindings.

Here a simple example for the new syntax:

```rust
fn factorial(x: i32) -> i32 {
    loop (result, count) = (1, x) {
        if count == 1 {
            break result;
        }
        (result * count, count - 1)
    }
}
```

@Centril Centril changed the title Create 0000-local-loop-bindings.md RFC: Local `loop` bindings Dec 25, 2018

Show resolved Hide resolved text/0000-local-loop-bindings.md Outdated
Show resolved Hide resolved text/0000-local-loop-bindings.md Outdated
Show resolved Hide resolved text/0000-local-loop-bindings.md Outdated
Show resolved Hide resolved text/0000-local-loop-bindings.md Outdated
Show resolved Hide resolved text/0000-local-loop-bindings.md Outdated

Especially since loops can return values, it's not necessary at all to mutate state inside a loop in some cases.

This is a more functional programming style, which may also allow more optimizations like storing the loop arguments in registers instead of allocating storage for mutable variables.

This comment has been minimized.

@Centril

Centril Dec 25, 2018

Contributor

This paragraph could use some elaboration + justification.

This comment has been minimized.

@porky11

porky11 Dec 25, 2018

I'm not sure, how to explain. I won't do this yet

This comment has been minimized.

@Centril

Centril Dec 25, 2018

Contributor

Alright; take your time. :)

This comment has been minimized.

@scottmcm

scottmcm Jan 8, 2019

Member

FWIW, mutable local variables are regularly converted to registers by LLVM. I wouldn't expect any optimization differences here, especially since the new syntax would disappear by the time we get to MIR anyway.

Show resolved Hide resolved text/0000-local-loop-bindings.md
Show resolved Hide resolved text/0000-local-loop-bindings.md Outdated
}
}
}
```

This comment has been minimized.

@Centril

Centril Dec 25, 2018

Contributor

An alternative desugaring covering refutable and irrefutable patterns would be:

loop PAT = EXPR {
    BODY
}

==>

{
    let mut tmp = EXPR;
    loop {
        match tmp {
            PAT => tmp = { BODY },
            _ => break, // If the pattern is irrefutable this will never happen.
        }
    }
}

In particular this lets us write:

loop (mut x, false) = (5, false) {
    x += x - 3;
    println!("{}", x);
    (x, x % 5 == 0)
}

Not sure whether this is a good thing, but it seems possible to extend your construct to refutable patterns.

This comment has been minimized.

@porky11

porky11 Dec 26, 2018

Adding this to section "Future possibilities" or maybe "Alternatives" seems better to me.
I'm not sure, if it's a good thing either.
In Scopes it's not, but it also doesn't have inbuilt variant types, so this won't help.

This adds more options to the language, which also makes the language more complicated, but it should be pretty intuitive, how it works.
# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

This comment has been minimized.

@Centril

Centril Dec 25, 2018

Contributor

Maybe this is bikeshed, but personally, I would find it good to reuse let pat = expr so that you write:

loop let (mut x, done) = (5, false) {
    if done { break; }

    x += x - 3;
    println!("{}", x);
    (x, x % 5 == 0)
}

This seems more consistent with the rest of Rust and it also immediately tells the user that what follows after let is a pattern.

This comment has been minimized.

@porky11

porky11 Dec 26, 2018

I thought, while let and if let are extended versions of if and while, which take bools.
Here we don't have arguments anyway, so let is not necessary.
But when having a let, would it be better to accept refutable patterns, too, for consistency with while let and if let?

Also when thinking about adding more possible keywords after while and if not having a let only after loop, but also having the same new keywords (maybe while const, if const and loop const) will be inconsistent.

So yeah, loop let is the syntax, I'd prefer, too.

This comment has been minimized.

@Centril

Centril Dec 26, 2018

Contributor

I thought, while let and if let are extended versions of if and while, which take bools.
Here we don't have arguments anyway, so let is not necessary.

I don't think it's for necessity; it's clearly not necessary syntactically; but it helps with clarity of highlighting that a pattern comes next. Just by seeing let p = q I know that p is a pattern and q is an expression. This reduces the overall complexity of adding loop let. With loop p = q { .. } that isn't as clear and complexity costs increase.

But when having a let, would it be better to accept refutable patterns, too, for consistency with while let and if let?

Sure 👍

Also when thinking about adding more possible keywords after while and if not having a let only after loop, but also having the same new keywords (maybe while const, if const and loop const) will be inconsistent.

Not sure what while const would be...

So yeah, loop let is the syntax, I'd prefer, too.

❤️

To avoid confusion, it would be possible to require a `continue` branch to repeat. Any branch reaching the end without `continue` would fail.
It would also be possible to just have labeled blocks with bindings, similar to "named let", as known from Scheme. In this case, reaching the end of the block will just leave the loop and go on afterwards.

This comment has been minimized.

@Centril

Centril Dec 25, 2018

Contributor

Could you elaborate on this with a code example of what this would look like?

This comment has been minimized.

@porky11

porky11 Dec 26, 2018

It's really not straightforward to add a syntax for this case.
I had something like 'label: let ... { body } in mind, but this is weird.
Maybe 'label x = y: { body } will work.
Loop could then look like this: 'label x = y: loop { body }
The only difference is, if the block returns or repeats by default. break and continue would do the same.
But requiring a label for this feature seems stupid anyway, so it's not a generalization at least. You probably don't want blocks to repeat anyway. Maybe I should delete this again.

Centril and others added some commits Dec 25, 2018

Suggested change for summary
Co-Authored-By: porky11 <Krapohl.f@gmx.de>
Update text/0000-local-loop-bindings.md
Co-Authored-By: porky11 <Krapohl.f@gmx.de>
@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 26, 2018

Thanks for the proposal, @porky11. Interesting idea! I definitely think this has some use cases. A few questions though:

  • Have you surveyed code/crates "in the wild" for how often this pattern is used? The concern with all new RFCs is whether the increase in complexity of the language outweighs the increase in power or ergonomics.
  • Like @Centril suggested, what do you think of the loop let syntax? It certainly makes it more consistent with other syntax, like while let and if let. However, my worry with this is that users will expect the dynamic semantics to be like while let. Or if the pattern matching is made refutable, then we get a situation where it's often not clear whether to use while let or loop let, and indeed the significant difference in semantics would be hidden by the very similar syntax.
@porky11

This comment has been minimized.

Copy link

porky11 commented Dec 26, 2018

* Have you surveyed code/crates "in the wild" for how often this pattern is used? The concern with _all_ new RFCs is whether the increase in complexity of the language outweighs the increase in power or ergonomics.

Not yet, but I already have it in mind.

* Like @Centril suggested, what do you think of the `loop let` syntax? It certainly makes it more consistent with other syntax, like `while let` and `if let`. However, my worry with this is that users will expect the dynamic semantics to be like `while let`. Or if the pattern matching is made refutable, then we get a situation where it's often not clear whether to use `while let` or `loop let`, and indeed the significant difference in semantics would be hidden by the very similar syntax.

I just replied to it. Also a good point, that loop let could easily be confused with while let. With just loop, this won't happen that easily, I assume.
Maybe even loop for just irrefutable patterns and while/if loop for just refutable patterns, analogous to let and while/if let? (just an idea, I'm not really considering it)

@burdges

This comment has been minimized.

Copy link

burdges commented Dec 26, 2018

Just fyi, you can build this with standard iterator adaptors now:

cycle(()).try_fold(
    (1, x), 
    |(),(result, count)| {
        if count == 1 {
            Err(result);
        } else {
            Ok((result * count, count - 1))
        }
    }
).unwrap_err()

In practice you'd be doing something succinct like

foo.iter_mut().try_fold(start_x, |i,x| foo(i?,x) ?) ?;

of (1..=x).rev().product() in this case.

I'm nervous about yet more complex loop structures. We'd gain more from conventions for error propagation from inside closures, of whichtry_fold gives a nice example.

@alexreg

This comment has been minimized.

Copy link

alexreg commented Dec 26, 2018

Maybe even loop for just irrefutable patterns and while/if loop for just refutable patterns, analogous to let and while/if let? (just an idea, I'm not really considering it)

Yeah... I've thought over the alternatives here a bit, and I can't come up with one I'm totally satisfied with. I kind of feel this is a consequence of having two syntaxes (while and loop) for what's traditionally just a while loop.

@porky11

This comment has been minimized.

Copy link

porky11 commented Dec 26, 2018

cycle(()).try_fold(
    (1, x), 
    |(),(result, count)| {
        if count == 1 {
            Err(result);
        } else {
            Ok((result * count, count - 1))
        }
    }
).unwrap_err()
foo.iter_mut().try_fold(start_x, |i,x| foo(i?,x) ?) ?;

These implementations are more confusing instead of expressing what's really going on.
When not used to rust error handling and advanced functional programming, this is difficult to understand.

(1..=x).rev().product()

This is pretty nice, but won't work for most cases.

@burdges

This comment has been minimized.

Copy link

burdges commented Dec 26, 2018

I think familiarity with ? is obligatory for rust developers, while the syntax here clearly adds "strangeness" overhead. If you want to write so that imperative developers not familiar Rust can read it easily, then you'll never find a syntax better than:

{ let mut x = start; loop {
    x = foo();
} }

If you accept to write for only a passing familiarity with Rust, then you can write more concisely by adopting the functional programming conventions like fold, etc. and use ? liberally.

In effect, the syntax proposed here creates an imperative "fold" operation, which improves conciseness, but only by being an unfamiliar syntax. Really, you should follow functional programming conventions if you want to do this both concisely and readably, meaning fold, etc.

We have no exceptions in Rust so our error handling requires tweaks like try_fold. Avoiding the try_ tweaks is afaik the only benefit of this RFC's syntax. I do not foresee much improvement there because functional languages commonly lack true exceptions. In fact try_fold is already more user friendly than solutions used by functional languages, in that it avoids language like "lifting into an exception monad".

We might discuss if some restricted form of closure could admit a limited form of exception. Imho, we should instead focus on making Resut<T,!> more equivalent to T to reduce the T vs. Result<T,E> complexity throughout the language.

@H2CO3

This comment has been minimized.

Copy link

H2CO3 commented Dec 27, 2018

This will not be a breaking change, since it's not allowed to have values other than () from a loop.

No, this is not true. A loop can yield a value through break.

Otherwise, I'm sorry to say but I dislike this proposal quite much.

  • The syntax is not intuitive, in fact it's not even evocative of the semantics.
  • The motivation is weak: the new syntax doesn't add anything new that a loop couldn't do previously. It introduces a lot of complication to the meaning of a loop for little reward. In fact, I find the reward disputable.
  • The recursion-like self-reference of the binding is implicit and magical (unlike an actual recursive function, where you can see the function call marking the point of recursion) – in other words, it's confusing.
  • The feature encourages procedural-style, loop-based code even for situations where a functional-style construct (e.g. fold or scan) would be entirely appropriate.

Furthermore, considering the factorial example:

This is, how you would define factorial using a loop now:

fn factorial(x: i32) -> i32 {
    loop (result, count) = (1, x) {
        if count == 1 {
            break result;
        }
        (result * count, count - 1)
    }
}

This is not any easier than:

fn factorial(x: i32) -> i32 {
    let mut result = 1;
    for count in 1..=x {
        result *= count;
    }
    result
}

In fact, the proposed version is longer and less clear. In a bigger, more complicated loop, I wouldn't like to have to trace all the occurrences of the changing bindings. I feel that writing code in this style would cause more harm than good.

A couple more points:

When not used to rust error handling and advanced functional programming, this is difficult to understand.

A simple fold can hardly be considered "advanced functional programming", I think that's an exaggeration. And the solution to being unfamiliar with Rust error handling is educating oneself about Rust error handling, not adding even more complexity to the language. In the big picture, growing the language makes learning it harder, not easier.

I also fail to see how the proposal would be beneficial for someone who is not (yet) well-acquainted enough with Rust to recognize the ubiquitous ? operator. These people would be either beginners/newcomers or occasional users:

  • For beginners, learning yet another loop construct is just more burden, it's not helpful.
  • For one-off users who aren't willing to write or maintain Rust code for a long time, it also doesn't serve a purpose: they only want a one-time solution using the most basic language constructs only, and the code won't be read or updated many times after.
@clarcharr

This comment has been minimized.

Copy link
Contributor

clarcharr commented Dec 28, 2018

This seems a lot to me like trying to emulate C-style for loops. I'm not sure I like it.

@alercah

This comment has been minimized.

Copy link
Contributor

alercah commented Jan 2, 2019

Alternative random idea: what if continue could take an expression inside a while let loop that would be used in place of the original?

fn factorial(x: i32) -> i32 {
    while let (result, count) = (1, x) {
        if count == 1 {
            break result;
        }
        continue (result * count, count - 1)
    }
}

Probably you should disallow implicitly or explicitly continuing with no value in this case, to avoid accidentally reevaluating the original let binding.

@mark-i-m

This comment has been minimized.

Copy link
Contributor

mark-i-m commented Jan 2, 2019

That would be a bit strange... I think break and continue should similar things to other languages.

@porky11

This comment has been minimized.

Copy link

porky11 commented Jan 3, 2019

@alercah
I also considered that. But it's just a different syntax. So instead of using different keywords for different kinds of loops, the same keywords will be used instead.
So this loop will work different depending on if the pattern is refutable, which will be confusing.
It may be possible allow continue with arguments in both cases, which would be even more confusing because the loop condition would not be evaluated in every iteration:

let mut i = 0;
while let Some(x) = list.pop() {
    i += 1;
    println!("iteration {}", i);
    x.handle();
    if let Some(child) = x.get_child() {
        continue Some(child)
    }
}
assert!(i == list.length())

I don't think, it will be intuitive, why in some cases, the assertion would fail.

@cramertj

This comment has been minimized.

Copy link
Member

cramertj commented Jan 8, 2019

@rfcbot fcp close

I think there's a pretty high bar for adding new, unfamiliar syntax extensions to everyday constructs like loop, and I don't think the feature proposed here (which looks to me like a built-in fold construct) meets that bar. This code is already expressible via a number of other means, such as regular loop statement with a mutation at the end of each iteration and the std::iter::fold method. Additionally, I don't believe that specifying loops in terms of folds is more Rust-idiomatic than iterative mutation, and so I don't believe that fold warrants built-in syntax.

@rfcbot

This comment has been minimized.

Copy link

rfcbot commented Jan 8, 2019

Team member @cramertj has proposed to close this. The next step is review by the rest of the tagged teams:

No concerns currently listed.

Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@aturon

This comment has been minimized.

Copy link
Member

aturon commented Jan 8, 2019

@rfcbot reviewed

@burdges

This comment has been minimized.

Copy link

burdges commented Jan 9, 2019

I think the practical take away: There are many closures for which try_* variants do not make great sense, like Iterator::map/filter/etc. It'd be nice if any "Effective Rust" books demonstrates concise error handling from these. I can imagine either macros or generators helping in some cases.

@kestred

This comment has been minimized.

Copy link

kestred commented Jan 9, 2019

I feel that this RFC as written introduces new syntax functionality in an unexpected and harder to teach way for newcomers to the language (and probably also for regular rust users who don't also actively follow RFCs), compared to the following currently supported transformation:

while let Some(x) = y.next() can be shortened to for x in y; which is sugar that is inherently intuitive (at least for native English speakers)

IMO, this problem stems from the fact that loop doesn't have any language that implies a binding.

For that reason, if this RFC were to be re-opened eventually, I'd heavily lean towards the loop let ... construction (as discussed above), or another alternative (loop do?), which would provide an indication to both new and experienced rust programmer, that they need to learn something new and also avoid potential conflicts with other future changes to let patterns (see below).

Introducing loop let might conflict with (arguably, but possibly controversially) future hypothetically valuable changes such as making let pattern be an expression of boolean type (proposed in another RFC, I'll go fetch a link).

In that case introducing loop let might be completely redundant functionality with respect to while let, but additionally would be decreasing consistency rather than increasing consistency as suggested in #2617 (comment), because the control constructs would only have let patterns by virtue of having an expression, whereas loop is currently expressionless (and giving it an expression would make it redundant with respect to while).

All of that to say that I agree with detractors of the RFC that the complexity introduced here doesn't justify the benefits currently described in the motivation, and I support the close disposition.


Edited: As an aside, I don't see while let mut mentioned in the above conversation, which seems like it might be able to accomplish the same thing as this RFC? Likely there is some semantic difference I haven't thought through...

@rfcbot

This comment has been minimized.

Copy link

rfcbot commented Jan 9, 2019

🔔 This is now entering its final comment period, as per the review above. 🔔

@graydon

This comment has been minimized.

Copy link

graydon commented Jan 12, 2019

Opposed. Lisp-ism that (as much as I love Lisp) would just confuse most Rust readers.

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