Eliminating "seemingly unnecessary" braces

It seems to me it already has been, with the for match proposal:

Which seems to solve at least part of the original motivation (maybe the main one?) while addresseing the concerns.

That's way too little motivation for yet more syntactic sugar. It's not at all obvious, to say the least, so it wouldn't help with readability. (I'd say it's downright detrimental to readability.)

I also don't get why people are so opposed to indentig two levels if there are two levels of language constructs involved. for match is not a thing. It's two things. There's no need to save two tab stops.

5 Likes

else if is is not a thing. It's two things. There's no need to collapse the two separate ideas into one construct.

If it's common to match on the item you're for-iterating over, then it's potentially worth adding a combined construct to do it in one step.

6 Likes

In some fully unscientific grepping on rustc, I get ~.5% of all fors are a for-match. I'm pretty sure some of those are false positives, but we might pick up some extras that look like

for x in xs {
  let x = some_simple_fn(x);
  match x {
    ...

Meanwhile the ratio of else if to if is more like 25%. I don't know where the line is for this syntax to support its own weight. I imagine there are some much more for-match heavy codebases than rustc.

2 Likes

else if is somewhat special, though. It's the same construct repeated/chained after another instance of itself. Furthermore, else if is a natural thing to say even in plain English. for match? Not so much.

1 Like

Also else if cannot be used alone. While for and match can.

2 Likes

To continue the bike-shed, how about:

match foo [over|in|of|from] optional_foo_iter {
   Some(x) => ...
   None =>  ...
}

or

match each foo [over|in|of|from] optional_foo_iter {
   Some(x) => ...
   None =>  ...
}

I mean: do other languages (haskell, ocaml) have similar kinds of proposed construct ?

In Haskell you would use a higher order function for a loop and there is the "lambda case" syntax extension that allows for using pattern matching and multiple branches directly in an anonymous function. This looks like

for_ myCollection \case
  Just x -> {- do something with x -}
  Nothing -> {- do something on Nothing -}

Where Just and Nothing are the Haskell equivalent of Some and None and Iʼm also using another syntax extension "block arguments" to avoid having to write a $ before the \.

Edit: to clarify, Haskell does not even have any special syntax for for-loops in the first place, and “translating” the lambda-case syntax to Rust would roughly look like

my_collection.for_each(|match| {
    Some(x) => /* do something with x */,
    None => /* do something on None */,
})

where

|match| { /* ... */ }

would be new syntax sugar translating to

|_x| match _x { /* ... */ }

similar to how in Haskell with the LambdaCase feature

\case {- ... -}

translates to

\_x -> case _x of {- ... -}
2 Likes

What would be the pros and cons of adding a keyword (e.g. rest) to represent the rest of the block instead? Something like:

for x in xs {
    let y = ...;

    match x rest;

    Foo(a) => ...
    Bar(b) => ...
    ...
}

It could seamlessly support other kinds of control flow (e.g. while true rest, if expr rest) as well as extra code above the line containing the keyword (like the let y above), so it could be general enough to not be too niche of a feature. Whosoever chooses could also opt for formatting like this if they wanted to as well:

for x in xs { match x rest;
    Foo(a) => ...
    Bar(b) => ...
    ...
}

Sorry, but I'm reeeeaally not a fan of this idea! As far as I can see:

Advantages:

  • saves an indent and set of braces

Disadvantages:

  • hard to read: have to visually scan to find the ... rest statement, can't go by indentation
    • with if ... rest et al, I may not even see it's there
  • does not really compose: can I write one ... rest statement after another? How often would that be useful?
  • only really works with () and ! types, since there is no place afterwards to use any return value,
    • unless with something like return match x rest; - that then would be a return that is followed by other code in the same indentation - again, very easy to overlook.
  • really error-prone during editing - it'd be the estranged cousin of "goto fail;".
  • a wholly alien syntax for the language

To add to the last point: Rust uses braces even for mods, where other languages use module foo; at the top of the file. To not do this, but use a similar construct within the actual code where it's more error prone would be strange indeed, IMHO.

With else if one creates a set of branches that depend on multiple values, which is not so uncommon. OTOH, a combined for and match is only useful on a single value that needs neither pre-, nor post-processing. As such I doubt it'd be handy all that often.

That said, since bikeshedding is fun, the straightforward syntax to me would be (Edit: just saw that this was one of @mcy's original suggestions):

for x in xs match x { 
    // ...
}

While that does duplicate the variable in the most trivial cases, it'd afford some opportunity for pre-processing:

for name in names match name.to_uppercase() {
    // ....
}

This could even be chained to a Python-esque

for record in records for order in record.orders match order.code {
    // ...
}

Disclaimer: not really proposing this, just exploring.

1 Like

It's probably worth adding that some languages considered it special enough to make it literally "one thing" with an elif keyword.

I'm not aware of any language doing something similar for its equivalent of for match (which AFAICT is still the only suggested motivating example in this thread).

5 Likes

Many languages treat ifthen … [else if …]* [else …]? specially. For example, in Ada the if statement forms a comb structure:

   if condition then
      sequence_of_statements
   [elsif condition then
      sequence_of_statements]*
   [else
      sequence_of_statements]?
    end if;

Points taken. That being said, I thought the whole point of the topic is to save on indentation. Or is it really the case that it's the braces that people are having a problem with?

I was hoping the lack of otherwise obligatory {braces} would be enough of a giveaway. I don't think that this is fundamentally unfixable though, e.g. allowed usage could be restricted to the first statement of a code block, or the keyword could have a different location and/or name: append_rest_of_block_keyword if/for/while/match ...;.

Personally, I think putting it on its own line would make it more verbose/explicit than shorthands like for match, so I don't think it'd be too easy to miss.

I don't see why it couldn't compose - each rest could be further "nesting" what follows. It could also uniformly work with all other control flow, since it's literally just adding a couple of braces around what's left within the code block.

Assuming the syntax could be made more obvious, would you feel the same? If so, could you please elaborate?

I see things the other way around on this front: other language's module foo; is rather more equivalent to Rust's way of using filenames, so I'd argue Rust is more implicit about it than other languages on this front. That being said, I could see a rest-like keyword working for pretty much any situation where excessive nesting is a problem. I see this as a strength, since it'd handle all cases uniformly.

I can't speak for everyone on this, but personally I write rather dense code (which is nonnegotiable because it means I can have more views into various code snippets of the project at a time, given fixed screen real estate; a corollary is that I don't use rustfmt at all), and in the crates I'm working on for work there's a lot of "base level" indentation i.e. method bodies start at column 12 or 16. This leaves relatively little room for code if a line happens to be long. One way to deal with that is to get rid of any indentation deemed superfluous, and a match nested in a for loop is often a prime candidate for that by collapsing the 2 into 1 and accepting that the "extra" pair of curly braces is an eyesore in that case.

2 Likes

I liked this idea so much, I learned to make procedural macros to make a macro that does what OP proposed: block_effects. You can chain multiple block effects to create just one block from nested blocks.

1 Like

I like the more bounded for match x in xs { arm1 => res1, arm2 => res2, } style proposed above and feel it is the one that "fits" enough to warrant consideration. I personally am definitely the sort of person to write this construct because I have a match-heavy mindset, though.

I believe there have been conversations that focused around if let and while let being "buffed" somewhat in this domain. I think those would be more worth consideration. if let is everywhere, so for let is also worth some consideration, but I have not seen much while let, so I am not sure if for let would get more usage?

I think a big part of that is that for is while let -- it's while let Some(x) = it.next() { (+ some details about IntoIterator).

2 Likes

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