Shorter syntax for operators passed to higher-order functions

I was wondering whether there's an interest in allowing for a more concise way to pass operators to higher-order functions, like map.

For example:

// Current way, example 1
let nrs = [1, 2, 3, 4];
let new_nrs = nrs.map(|nr| nr + 1);
// Possible new way, example 1
let new_nrs = nrs.map(+1);

Thinking further, allowing two-tuples to be passed to a binary operator, like ^:

// Current way, example 2
let xored = nrs.zip(new_nrs).map(|(n1, n2)| n1 ^ n2);
// Possible new way, example 2
let xored = nrs.zip(new_nrs).map(^);
2 Likes

This is quite ambiguous since that is also a positive literal integer, not some kind of shorthand lambda. Given my understanding of Rust's preference for unambiguous syntax (cf. turbofish), this won't work as presented and some other syntax will be required. You also have ambiguities with *var for right-hand multiplication (I don't think operator overloads have to abide by commutativity, just precedence orders and they get the default associativity). I suspect this has been discussed before, but I don't have the links myself.

7 Likes

Yes, this comes up every once in a while. TL;DR: it does more harm than good.

6 Likes

Regarding

let new_nrs = nrs.map(+1);

I also thought of Haskell's way of writing this as nrs.map((+1)), but optimistically opted for the slightly shorter syntax without the parentheses.

Playing around with it a bit in ghci:

Prelude> :t (+1)
(+1) :: Num a => a -> a
Prelude> :t +1

<interactive>:1:1: error: parse error on input ‘+’
Prelude> :t -1
-1 :: Num a => a
Prelude> :t (-1)
(-1) :: Num a => a
Prelude> :t (- 1)
(- 1) :: Num a => a

It would seem that anything ambiguous is taken to be an integer literal. I don't think that (+1) being a lambda while (-1) is an integer makes me feel happy about it (yes, Haskell has this weird dichotomy, but flip (-) 1 is natural enough there whereas Rust doesn't have anything that concise at hand).

Another option for a short-hand syntax might be something like (_ + 1) and (_ + _).

11 Likes

This is nice, especially for operators that are not commutative such as -:

(1 - _)

I don't know if something like nrs.map( 1- ) is too terse for some tastes.

In my opinion, an expression like pairs.map(_ + _) is just asking to be reduced to pairs.map(+).

1 Like

Well, except that the closure is already shorter than that?

flip (-) 1
|x| x - 1

When the closure is short enough for this kind of sugar to be potentially-good, the name for the parameter can also be super-short.

So given that Rust's closure syntax is already quite short, I've never felt that it was worth adding the extra magic for this. It's not like we're old-C# where it was delegate int (int x) { return x - 1; } or old-C++ where it was std::bind2nd(std::minus<int>(), 1) -- both of those languages did add new shorter syntax because the old one was too long.

9 Likes

I quite like Rust's usual preference of explicit vs implicit. |x, y| x + y and |(x, y)| x + y are conveniently explicit. _ + _ is just explicit enough IMO. Anything less than that is too implicit.

I'd really like to see your breakdown of pros and cons of these options. If the only "advantage" of (^) and (1-) is that they save 4 characters then I personally think that is far from sufficient to justify the disadvantages.

2 Likes

I'll note that that could be any of the following:

|x| x + x
|x, y| x + y
|x| |y| x + y

Not to mention that once it's in context it's even less obvious, as one needs to make a rule why foo.map(!_) is foo.map(|x| !x) instead of |x| foo.map(!x).

(See why Boost.Lambda ends up with features like https://www.boost.org/doc/libs/1_62_0/doc/html/lambda/le_in_details.html#idm45927935287616)

Now, obviously we could write rules for which one it is, or restrict it to only only one argument, or a variety of other such things. But it's not at all clear to me that the value of omitting the |x| is worth all that complexity.

If you're skeptical of one of those expansions, let me suggest that let squares = (0..n).map(_ * _); looks reasonable.

9 Likes

The big problem with _ + 1 as a shorthand for |x| x + 1 is defining the scope of the closure. What does 1 + _ + 1 act as? Is it 1 + |x| { x + 1 } or |x| { 1 + x + 1 }? What about foo(_)? It could be useful to have that as either |x| { foo(x) } or foo(|x| { x }), and either could be intuited. Consider also bar(1, _).

The consensus the last time this _ closure syntax was discussed is that it's way too (human) ambiguous.

Actually, no, Rust doesn't support + in number literals, nor prefix +. Stable 1.56 gives the unhelpful error

error: expected expression, found `+`
 --> src/main.rs:2:13
  |
2 |     let _ = +1;
  |             ^ expected expression

but we've improved it in beta 1.57 to the more helpful

error: leading `+` is not supported
 --> src/main.rs:2:13
  |
2 |     let _ = +1;
  |             ^ unexpected `+`
  |
help: try removing the `+`
  |
2 -     let _ = +1;
2 +     let _ = 1;
  | 

In macros, though, the error is still quite unhelpful

error: no rules expected the token `+`
 --> src/main.rs:2:10
  |
2 |     dbg!(+1);
  |          ^ no rules expected this token in macro call

and this hints at why this would be a (small, editionable) breaking change: macros can currently match a leading + explicitly just fine, and making it part of the number literal would break such macros.

(The sentiment holds for -1, of course.)

3 Likes