Lambda syntax

it's not convenient to use pipes in lambdas, why don't use syntax like in other languages? For example: .map(x -> x+1) or .map((x,y) -> x+y)

(x,y) -> expr requires infinite lookahead as it isn't immediately clear if (x,y) is a tuple expression or the argument list of a closure. This comes at the cost of a slower parser in the compiler and in your head. For simple cases it may not have much if a cost, but once you get to more complicated cases you quickly get much more overhead. In addition -> is already used for return type annotations: |a,b| -> u8 { 42 }.

I am confused. Which part of this makes the amount of lookahead required to be infinite? The tuple? I thought rust's lexer emitted token trees, so that (x, y) is effectively a single "parentheses" token.

The lexer emits tokens. Only macros work with token trees.

3 Likes
struct Struct {
    foo: i32,
    bar: i32,
}

fn main() {
    let x = (Struct { foo, .. }) -> foo + 1;
    println!("{}", x(Struct { foo: 42, ..Default::default() })); // should print 43
    // vs
    let x = |Struct { foo, .. }| foo + 1;
    println!("{}", x(Struct { foo: 42, ..Default::default() })); // prints 43
}

Destructuring in closure arguments is allowed, with the alternative syntax we wouldn't be able to tell whether (Struct is starting a tuple or a closure, and if we decide to try to parse the tuple, we'd get a parse error because of the bare .. belonging to a pattern, not an expression.

3 Likes

For one reason, because the syntax is stable and fixed. Given Rust's stability guarantees, it will not change.

7 Likes

"Infinite" here means the number of tokens needed to know if it's a lambda is arbitrary, rather than a fixed-length low number, like one or two tokens.

This syntax is from another language: Ruby.

But it's 6 years too late to change it. Rust promised in 2015 that it won't do such things.

6 Likes

Note that in C#, which has almost exactly that syntax (just => instead of ->), the syntax is really awkward for autocomplete. Because when you type .map(x the IDE can't tell whether you're trying to reference a variable or type named xyz -- in which case it should provide intellisense hints -- or whether you're about to make a lambda and are trying to make a new binding -- in which case it really shouldn't autocomplete the identifier.

In Rust, however, .map(|x is clearly a new binding because the start of a closure's parameter list is the only option after a non-infix |. (In general, Rust is pretty good about having "here's what's coming" markers in the syntax. This is one of the reasons that let x: i32 = 4; is superior to i32 x = 4;, for example.)

12 Likes

It would help us understand why you make this change request, if you could explain why you find it inconvenient to use pipes in lambdas. For me, there is no real difference in difficulty to type |x| x+3 or x -> x+3. Are you using a keyboard layout where | is awkward to get at, or something like that?

3 Likes

I'm sorry, but what? What can possibly not be "convenient" about something so trivial as this? I simply don't buy that.

Rust does a lot of things differently from other languages. There is no point in designing a new language if all you do is copy existing languages.

One of the primary cornerstones of Rust is correctness and predictability. The projection of this principle to syntactic choices is that syntax should be unambiguous and easy to parse for humans as well as automatic parsers.

"Easy to parse" doesn't necessarily mean "familiar from another language". You can be familiar with languages of an awfully context-dependent syntax (such as C++). They do not count as "easy to parse" by any standards. Rust chose not to repeat the mistakes of languages with heavily context-dependent syntax, by changing the form of some language constructs when it was necessary.

1 Like

Let’s not bikeshed over already-decided syntax.

7 Likes