Loop-match, for-match

this has already been discussed, but it was pre 1.0: Pre-RFC: loop/for/else match

But priorities have shifted from getting a stable feature set, to making things nicer, so I think this deserves another look

the idea is to have loops like this:

loop match expr {
    Foo(x)=>{...}
    Bar(x)=>{...}
}

for match in iter_expr {
    Foo(x)=>{...}
    Bar(x)=>{...}
}

which would roughly desugar to:

loop {
    match expr {
        Foo(x)=>{...}
        Bar(x)=>{...}
    }
}

for item in iter_expr {
    match item {
        Foo(x)=>{...}
        Bar(x)=>{...}
    }
}

It'd be good for dropping a level of braces for event loops and such

the for loop syntax i'm suggesting here is definitely more awkward than loop. it'd be nice to have some consistency and allow a match to replace any block (if/else bodies, functions, while loops, etc), but i can't think of a nice syntax for those cases.

Well, if and while don't bind anything, so this doesn't really apply there. For functions, well, at least for single argument closures one could use something like |match| { /* match arms */}. This has some precedent in Haskell, I've explained it in more detail in this post in a thread that was also about eliminating extra indentation levels:


Thinking about it again now, one could even consider supporting e. g. |match, match, match| { (a, b, c) => {...} }, bundling up multiple values in a tuple.

And one could go much further and generalize this to allowing match as a pattern in general in some places, e. g.

while let Some(match) = foo.next() {
    0 => {
        bar1();
        bar2();
    }
    n => baz(n),
}

and similar for if let and even function definitions

fn foo(match: Option<bool>, u: char, (match, i): (i32, u8)) {
    (Some(true), 0) => bar(), 
    (None, n) if n > 0 => baz(), 
    (_, n) => qux(), 
}

Edit: Going back to discussing your suggestions, this generalization would mean the for syntax stays as you proposed, and the loop match expr { ... } needs to be written as while let match = expr { ... }. Admitted, that's a bit of a keyword zoo, and a bit too while true-style, so perhaps loop match could be a new “abbreviation” of while let match =.

2 Likes

i was kinda thinking of something like the thread you linked. (if <cond> match <expr> { }) but i'm not a fan of that syntax, since it'd easily encourage either long lines or "goto fail" indentation

the single-argument lambda match (or first argument?) seem nice to me.

i think i have a strong preference for keywords to show up at the beginning of the expression, so they can't hide as easily

Another somewhat-related proposal from a while back: A fn match syntax sugar

loop match seems logical to me, because that's just optional braces. However, I don't write loop that often to appreciate a special shorthand syntax for it.

I can see else match or else <keyword> {} in general being a useful syntax sugar, and it wouldn't be too weird, because else if already exists in Rust, and C is even more permissive here.

However, I needed a triple double-take for for match and fn match. That's an unusual, very dense syntax. A magic variable name that changes syntax of a next block is too much for me.

12 Likes

Agreed on both counts.

I do like the idea of being able to write top-level matches more easily, but this syntax feels like it optimizes for writing at the expense of reading.

3 Likes

For me one of the biggest use of loop is to handle the lack of a default case in a for loop. IMO new syntax around for and loop should first try to solve that:

'outer: loop {
    for item in list {
        if cond { break 'outer f(item); }
    }
    break default_val();
}

you mean

list.into_iter().find(|item| cond).map_or_else(default_val, f)

or

if let Some(item) = list.into_iter().find(|item| cond) {
    f(item)
} else {
    default_val()
}

?

Edit: Yet another way, now that then is almost stable (already in beta):

list.into_iter()
    .find_map(|item| (cond).then(|| f(item))
    .unwrap_or_else(|| default_val())

or if you like blocks (and fighting against rustfmt)

list.into_iter().find_map(|item| {
    (cond).then(|| {
        f(item)
    })
}).unwrap_or_else(|| {
    default_val()
})
4 Likes

oh, i didn't parse it that way, but i can see how it'd be very easy to

i parsed it as (for match) in expr. like, a 2 word keyword. but it's just lowercase letters, so if you're not looking at it with syntax highlighting, it'd easily look like a variable name

maybe dropping the for would work better, and have match in be a thing?

match in iter_expr {
    Foo(x)=>{...}
    Bar(x)=>{...}
}

that would be useful for Option<Enum> and Result<Enum> code, which comes up often. i almost want it to handle the unwrapping, but no decent syntax is coming to mind

if x.is_none() {
  ...
}
else match x.unwrap() {
    Foo(x)=>{...}
    Bar(x)=>{...}
}

i like the match closures from there:

xs.map(match {
    Foo(x)=>{...}
    Bar(x)=>{...}
})

maybe less of a big deal, since you're not saving a level of indentation/braces on a closure like that

^^^ am I missing something or is this just a new, less readable alternative for

match x {
    None => {
        ...
    }
    Some(Foo(x)) => {...}
    Some(Bar(x)) => {...}
}

?

7 Likes

This is definitely not a case that needs "shorter syntax". A loop is a loop, a match is a match, and if you need both, you can just compose them.

Composability (which is already one of Rust's strengths) is a much better alternative over adding syntax for every possible combination of control structures. It's clearer and easier to learn/teach as well as easier to change.

If you have too much indentation in your code, the solution is not to incoherently lump together unrelated parts of it. The solution is to refactor and modularize. Too clever and dense code won't become higher-quality due to more syntactic sugar and cleverness (arguably, that only makes it worse). It becomes higher-quality due to design effort put into it being higher-quality.

9 Likes

Unfortunately neither of the syntax you proposed to simplify 'outer: loop { for ... { if ... { break 'outer ...; }; break ...; } works easily if you have any function inside that use the ? operator because lambdas can’t interact with the control flow of the caller.

I took a look at this proposal, as well as the link posted by @steffahn in the second post. While I agree that removing extra level of braces/indentation would be nice, I don’t think that loop match or for match should get specific syntax.


I am now highly off-topic, but personally the think that annoys me the most in term of extra indentation is this pattern:

match x {
    None => {
        return Err(...); // short exit early
    },
    Some(x) => {
        // use the unwrapped x
        ...
        // most of the code now has 2 extra level of indentation
    },
}

That’s two extra level of unwanted indentation, because lambda can’t modify the control flow of the caller. Assuming that the hypothetical inline keyword could do such thing, here is the kind of syntax I’d like:

let x = x.unwrap_or(inline || {
    return Err(...);
});
// x in now unwrapped
// the rest of the function doesn’t have extra indentation.

How about

let x = x.ok_or_else(|| Err(...))?;

or even

let x = x.ok_or(Err(...))?;

in case of ... being some literal/constant?

3 Likes

I will keep this is mind. That’s probably what I need. I’m surprised I didn’t saw it mentioned (maybe it was) in the last if/unless discussion I read here. If it effectively works everitime, you made my day!

1 Like

Why not take advantage of the fact that you early-return anyway?

let x = match x {
    None => return Err(...),
    Some(x) => x,
};

// use `x` here without any additional indentation

The verbosity is way above java level of buraucratie! :slight_smile:

I'm looking forward to let-else syntax.

3 Likes

It's a lot less verbose than your original… (and I'm now confused, because you originally wanted to reduce spurious indentation, so I'm feeling like we've got a moving goalpost here.)