-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
RFC: Local loop
bindings
#2617
Changes from 8 commits
7fea233
22567d9
174bc09
a842196
2308267
9f9ff59
a20af5f
7634e2d
ba934b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,245 @@ | ||
- Feature Name: local_loop_bindings | ||
- Start Date: 2018-12-25 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
To simplify complicated loop constructs and avoid mutable state, | ||
allow an extended syntax for `loop` to accept local variables that may change once per iteration. | ||
|
||
To get an idea of what this is about, here you already can see a simple example for factorial using the new syntax: | ||
|
||
```rust | ||
fn factorial(x: i32) -> i32 { | ||
loop (result, count) = (1, x) { | ||
if count == 1 { | ||
break result; | ||
} else { | ||
continue (result * count, count - 1); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
The chief motivation is to enable using different values for each iteration without the need of mutable bindings defined outside of the loop. | ||
|
||
The bindings will be defined after the `loop` keyword, making them only accessible in the scope of the loop, not afterwards. As usual, they will not be mutable by default, which helps to ensure, that the variables change at most once per iteration. | ||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This paragraph could use some elaboration + justification. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure, how to explain. I won't do this yet There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright; take your time. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
The extended syntax for `loop` looks like this: | ||
|
||
```rust | ||
loop binding = value { | ||
/* body */ | ||
} | ||
``` | ||
|
||
Just like when using `let` and unlike `if let`/`while let`, `binding` has to be be any irrefutable pattern, which means, that it will match for every value of the type. | ||
|
||
The return value of the loop body will implicitely be passed to the next iteration of the loop, so it needs to be the same type as the initial value. | ||
|
||
An example of a simple loop, which iterates the loop ten times and prints the iteration number, would look like this: | ||
|
||
```rust | ||
loop i = 1 { | ||
if i <= 10 { | ||
println!("iteration {}", i); | ||
i + 1 | ||
} else { | ||
break; | ||
} | ||
} | ||
``` | ||
|
||
`continue` will accept an argument in this loop, which will be passed to the next iteration. Using continue, this could look like this: | ||
|
||
```rust | ||
loop i = 1 { | ||
if i <= 10 { | ||
println!("iteration {}", i); | ||
continue i + 1; | ||
} | ||
break; | ||
} | ||
``` | ||
|
||
Since the end of the loop is never reached, the return value is not required to be the type of the binding, here. | ||
|
||
A loop without bindings (`loop { /* body */ }`) will be the same as this: | ||
|
||
```rust | ||
loop () = () { | ||
/* body */ | ||
} | ||
``` | ||
|
||
This will not be a breaking change, since it's not allowed to have values other than `()` from a loop. | ||
|
||
A simple example from the book looks like this: | ||
|
||
```rust | ||
let mut x = 5; | ||
let mut done = false; | ||
|
||
while !done { | ||
x += x - 3; | ||
|
||
println!("{}", x); | ||
|
||
if x % 5 == 0 { | ||
done = true; | ||
} | ||
} | ||
``` | ||
|
||
Using the new syntax, this could be rewritten as this: | ||
porky11 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```rust | ||
loop (mut x, done) = (5, false) { | ||
if done { | ||
break; | ||
} | ||
x += x - 3; | ||
println!("{}", x); | ||
(x, x % 5 == 0) | ||
} | ||
``` | ||
|
||
This is how you would define factorial using a loop now: | ||
|
||
```rust | ||
fn factorial(x: i32) -> i32 { | ||
loop (result, count) = (1, x) { | ||
if count == 1 { | ||
break result; | ||
} | ||
(result * count, count - 1) | ||
} | ||
} | ||
``` | ||
|
||
With explicit `continue`, it can look like this: | ||
|
||
```rust | ||
fn factorial(x: i32) -> i32 { | ||
loop (result, count) = (1, x) { | ||
if count == 1 { | ||
break result; | ||
} else { | ||
continue (result * count, count - 1); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Using `break` here allows copying code without having to modify it, when not using a specific function. | ||
|
||
Labels will also work. When using `continue` with a label, the arguments to continue must match the loop binding signature connected to the label, in case the label is connected with a loop. | ||
|
||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
The syntax extension should not cause any issues with backwards compatability. | ||
|
||
It's just an extended syntax for `loop` in a place, where currently nothing is allowed yet. | ||
|
||
The expansion of the new syntax will be shown for an example. | ||
|
||
New syntax: | ||
|
||
```rust | ||
loop (a, mut b) = (x, y) { | ||
/* body */ | ||
} | ||
``` | ||
|
||
Current syntax: | ||
|
||
```rust | ||
{ // ensure global bindings to be inaccessible after the loop | ||
let mut binding = (x, y); | ||
loop { | ||
let (a, mut b) = binding; | ||
binding = { | ||
/* body */ | ||
} | ||
} | ||
} | ||
``` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding this to section "Future possibilities" or maybe "Alternatives" seems better to me. |
||
|
||
This expansion should cover the common case. | ||
|
||
A `continue value` in the body would expand to `binding = value; continue;` | ||
|
||
Internally there may be more efficient ways to implement this. | ||
|
||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this is bikeshed, but personally, I would find it good to reuse 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought, Also when thinking about adding more possible keywords after So yeah, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
Sure 👍
Not sure what
❤️ |
||
|
||
It would be possible to extend `while let` instead, so it supports both refutable and irrefutable value and add additionally add support for `continue`, but in one case the expression generating the value is called for each iteration and in the other case only in the beginning, so this is probably not an option. | ||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you elaborate on this with a code example of what this would look like? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's really not straightforward to add a syntax for this case. |
||
This could be a more general version, which is not connected to loops, but can be used for everything, which can have labels. | ||
|
||
|
||
# Prior art | ||
[prior-art]: #prior-art | ||
|
||
## Scopes | ||
|
||
The main inspiration was the `loop` construct in the upcoming release of the [scopes programming language](scopes.rocks) ([this commit](https://bitbucket.org/duangle/scopes/commits/6a44e062e6a4a7813146a850c8982c0f902eefba)). | ||
Documentation is still raw and things may change, but the current version of loop matches best with rust. | ||
|
||
The same example of factorial should look like this in the next scopes release: | ||
|
||
```scopes | ||
fn factorial (x) | ||
loop (result count = 1 x) | ||
if (count == 1) | ||
break result | ||
else | ||
continue | ||
result * count | ||
count - 1 | ||
``` | ||
|
||
## Rust specific | ||
|
||
Without the feature of loops being able to return values, this feature is less useful. | ||
|
||
Labeled blocks, which are currently unstable, may also be useful for some alternative to this. | ||
|
||
|
||
|
||
# Unresolved questions | ||
[unresolved-questions]: #unresolved-questions | ||
|
||
There are some other design decisions, mentioned as alternatives, which could be taken into account instead. | ||
But I'm pretty sure, the proposal itself is more useful and straightforward than the alternatives. | ||
There are no unresolved questions yet. | ||
|
||
# Future possibilities | ||
[future-possibilities]: #future-possibilities | ||
|
||
If named blocks are stabilized, they could additionally allow local bindings, like a "named let". | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd encourage you to elaborate more in this section and provide more real world examples where this new control flow form would make existing code more readable / ergonomic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea to look at existing code. I just tried to figure out some useful examples myself and only came up with rewriting factorial.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe ;) You might also want to compare and contrast with iterators. For example, are there cases where this would make code that is hard to formulate with iterators (possibly due to borrowing...) easier to write?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think, such a comparison is that useful. The main cases for using this syntax are not for iteration over sequences. Even if something is possible to formulate using iterators, using loops may be easier to understand, especially, when not knowing enough rust to understand iterators.