Allow loops to return values other than ()

While completely unnecessary and probably a bad idea for 1.0, it would be kind of nice if one could return a value (other than ()) from a loop (I apologize if this has already been discussed).

For example:

let maybe_item = for item in haystack {
    if item == "needle" {
        break Some(item);
    }
    None
};
let name = loop {
    match parse_name(get_input()) {
        Some(name) => break name,
        _ => ()
    }
}

etc…

2 Likes

@stebalien It will be difficult to simply introduce such a feature because it confilcts with the “break with label” syntax.

Generally speaking, we should prefer higher order functions to the loop syntax. Please have a look into utilities like Iterator::find().

You can also use a closure enclosing the loop:

fn foo<'a> (haystack: Vec<&'a str>) {
    let maybe_item = (|| -> Option<&'a str> {
         for item in haystack.iter() {
            if *item == "needle" { return Some(*item); }
         }
         None
    })();
    println!("{}", maybe_item);
}
1 Like

I don't think it does, since those are always break 'name, i.e. syntactically distinguishable from an expression.

(The closure approach is a neat trick, and will become more flexible with unboxed closures.)

I like this. Perhaps the return value could be an option.

let maybe_item: Option<Item> = for item in haystack.iter() {
    if item == Needle {
        break item;
    }
};

match maybe_item {
    Some(item) => println!("Found {}, item),
    None       => println!("Found nothing")
}

I’ve thought of this too. For loops it’s totally straightforward because the only way the loop can exit is via break. So you can just add an optional value argument to break (default ()) and the loop will return it.

It’s much less obvious for while and forin. What’s the value if the loop exits without a break? Last evaluated expression in the loop body doesn’t seem like a good solution, because that’s evaluated at every iteration, making it wasteful, and also likely unusable for anything that’s not Copy. I also don’t really like the option of baking in Option (such ad-hoc). The best option I can think of at the moment would be to add an optional else block (mandatory if you’re returning something other than ()) which is evaluated iff the loop never breaks:

let maybe_item: Option<Item> = for item in haystack.iter() {
    if item == Needle {
        break Some(item);
    }
} else {
    None
};

While this seems perfectly semantically cromulent, it’s somewhat awkward and potentially confusing. (One might also think that e.g. the else is taken iff the loop never runs (condition fails immediately), which is not the case.)

In any case, I think we should go ahead and do this for loops because it’s easy and obvious, and put the other two loop constructs off until later.

Related feature: Early exit from any block. (The fact that we want to generalize break for loops no matter what suggests that it’s also break which should be used here, and not return.)

2 Likes

Actually, python has this exact feature (while/for … else) and it’s extremely useful in find or create cases.

2 Likes

Early exit from any block seems a lot more generally useful and orthogonal.

let maybe_item = 'my_block {
    for item in haystack.iter() {
        if item == Needle { break 'my_block Some(item); }
    }

    None
};

(Maybe we could also implicitly label blocks that are immediately used to initialize a variable with the variable’s name so this would just be let maybe_item = { ... break 'maybe_item Some(item); ... } …)

Other uses like short-circuiting out of a match arm but not out of the whole function also seem neat.

I really like the beginning sentence of the Scheme language spec:

Programming languages should be designed not by piling feature on top of feature, but by removing the weaknesses and restrictions that make additional features appear necessary.

I think this proposal has a Bad Smell based on this criterion. If Iterator::find() isn't really enough for your usage, we can add more utilities on the library layer.

4 Likes

First, this is rust, not scheme: http://doc.rust-lang.org/reference.html#statements-and-expressions. Regardless, the important part is not how many features the compiler supports, it’s how many features the user needs to learn. I’d argue that this proposal, from a learnability perspective, is a negative feature because it simply makes loops behave more like other expressions (if/match) and makes break statements behave more like return statements. While your closure example works, it’s much less readable (which, IMHO, is the most important feature in a language).

If you’re looking for a better example than the contrived find one, consider the following:

/// Without the feature
fn main() {
    let maybe_name;
    println!("Please enter your name or 'random' to be assigned a random name: ");
    loop {
        match get_input() {
            Some("default") => {
                maybe_name = None;
                break;
            },
            Some(inp) => {
                maybe_name = Some(inp);
                break;
            },
            _ => println!("You didn't enter a name... Try again: ")
        }
    }
    let name = match maybe_name {
        Some(name_tmp) => name_tmp,
        None => generate_name(),
    };
    println!("Hello {}", name);
}
/// With the feature
fn main() {
    println!("Please enter your name or 'random' to be assigned a random name: ");
    let name = loop {
        match get_input() {
            Some("default") => break generate_name(),
            Some(input) => break input,
            _ => println!("You didn't enter a name... Try again: ")
        }
    };
    println!("Hello {}", name);
}

Yes, this could be done with a closure but then you need an extra closure.

I like this feature proposal because it leverages the syntax … less helper functions to remember;

we see that Rust has an expression based syntax, and this completes it… it seems consistent, and elegant.

i’m sure there’s a way the break label/expression can be disambiguated

2 Likes

I use the corresponding feature in Python a lot, but to be fair the following two approaches already work in rust. I guess that they cover many use cases.

let item1 = items.iter().filter(|item| **item == Needle).next();

let item2 = items.iter()
                 .filter_map(|item| if *item == Needle { Some(item)} else { None })
                 .next();

EDIT: http://is.gd/T3LNib

Yes, the kicker as @glaebhoerl says is to make for loops also expressions. The for .. else trick solves that and allows us to do amazing things:

  • all blocks/loops are expressions, (C-style <=> type is () ).
    • Else unneeded <=> forces () type is consistent with if
  • completely combine return and break, and great rid of one ideally.
  • break 'a is sugar for break 'a (), and likewise for without the loop identifier.

Combine this with @glaebhoerl’s matching proposal, and the only control constructs we need in the core language are loop { .. }, match EXPR { .. }, break 'ID EXPR, and functions. That is wonderfully simple!

(Eventually, with a forced tail-call calling convention, loops and break could be desugared too but that’s a long way off.)

I certainly agree that this would be a good idea. for, while, and loop loops are practically useless as expressions currently, which doesn’t really fit with Rust’s general idea of everything-is-an-expression (or everything-should-be-useful-as-an-expression, anyway). Sure, iterators and their methods can be used for this, but there isn’t always an iterator/method for what you want.

A month or so ago I drafted an RFC for adding else clauses to loops and adding expressions to break statements, so I’ve put it up on GitHub for the purposes of this discussion. I don’t think there’s much point in actually submitting it as an RFC yet as there are a lot of much more important things to be done before 1.0.

My only concern with using break for early-exit-from-any-block is that, to stay consistent with existing precedent, as well as to be convenient for the common case, the default block to break out of if the lifetime argument is omitted would not be the innermost block, but the innermost loop. E.g.

while cond {
    match foo {
        Foo => {
            break
        }
        ...
    }
}

Here the break would exit the while, even though there are two other blocks inside of it which are closer to the break (and which could have been specified as the target explicitly). This might be a complete non-issue, but it also might be surprising for some people or in some cases. I dunno.

According to an earlier brief chat with @eddyb on IRC, Rust's ideal core language is actually a typed CFG. So there would be no actual source-level constructs remaining (though I guess you'd still need match).

“typed CFG” hmm, what do you mean?

Control flow graph. @eddyb could probably explain better; I just have a vague sense.

stebalien, the following code snippet seems perfectly reasonable to me (you didn’t provide get_input, so I just assumed it was doing some error checking):

/// Without the feature
use std::io::{stdin};

fn main() {
    println!("Please enter your name or 'random' to be assigned a random name: ");
    let name = stdin().lines()
        .filter_map(|line| line.ok())
        .map(|line| match line.as_slice().trim() {
            "default" => generate_name(),
            _ => line
        })
        .next()
        .unwrap();

    println!("Hello {}", name);
}

fn generate_name() -> String {
    "Tim".to_string()
}

@P1start Unless you personally have more important things to work on, I think it is worth submitting that as an RFC. Worst thing that can happen is it’ll get postponed. But small expression-level features like this do sometimes get accepted (tuple fields, if let).

OK, I went ahead and submitted the RFC. I figure that it’s backwards compatible (except perhaps some interactions with macros, but those might not be stable by 1.0 anyway), so whether or not it’s implemented before 1.0 doesn’t matter, so it doesn’t matter when I open it anyway, since 1.0 doesn’t affect it (assuming the core devs don’t bother working on it).

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.