Skip to content
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

Closed
wants to merge 9 commits into from
Closed
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 245 additions & 0 deletions text/0000-local-loop-bindings.md
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
Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Contributor

@Centril Centril Dec 25, 2018

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?

Copy link
Author

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.

[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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This paragraph could use some elaboration + justification.

Copy link
Author

Choose a reason for hiding this comment

The 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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright; take your time. :)

Copy link
Member

Choose a reason for hiding this comment

The 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 */
}
}
}
```
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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.
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 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
Copy link
Contributor

@Centril Centril Dec 25, 2018

Choose a reason for hiding this comment

The 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 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

❤️


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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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".