rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Implicit closure parameters

Open ElectricCoffee opened this issue 7 years ago • 21 comments

The current closure style is great. It's simple and to the point and leaves little to be desired.

However, I often find myself writing code like this:

some_operation().and_then(|x| x.get(1));

Where all I'm doing is call an accessor on a single argument. Not having to introduce a single temporary variable for use in short expressions is nice and useful in the case of short in-line expressions.

A few programming languages provide a sort of syntactic sugaring to this pattern, of which I will mention two.

Scala Style

The Scala style of implicit closure parameters is simply to use an underscore.

some_operation().and_then(_.get(1));

Pros: The underscore is already used for other things in Rust, most notably generic types and patterns.

Cons:

  • Yet another meaning for _.
  • Looks unnatural when referencing and dereferencing: &_, &mut _, *_

Scala's style also lets you do this for closures that take multiple parameters:

vec.iter().fold(0, _ + _);

which would be the same as

vec.iter().fold(0, |acc, x| acc + x);

Granted, I feel this is somewhat less readable.

Kotlin Style

Kotlin uses a special variable called it

some_operation().and_then(it.get(1));

Pros:

  • Doesn't use an underscore, and looks more like natural code.
  • Can be used with the existing reference and dereference operators &it, &mut it, and *it don't look out of place

Cons:

  • New users might be confused where the mysterious variable suddenly appeared from.
  • The variable also doesn't have any other uses, so new special rules would have to be applied to this one specific case.

Unlike Scala, Kotlin only uses this to replace a single variable.

This is more of a quality-of-life request moreso than anything, though I still feel it's worth bringing to attention.

Personally I'm split between the two syntaxes, the succinctness of _ is great and quick to type, but it somehow feels like it makes more sense.

ElectricCoffee avatar Oct 01 '18 17:10 ElectricCoffee

While I like succinctness, I prefer the current explicit design for a system language designed for reliability.

leonardo-m avatar Oct 01 '18 18:10 leonardo-m

Seems related to #1577

mark-i-m avatar Oct 01 '18 19:10 mark-i-m

I want to reserve _ for value inference (see const generics, etc.) since it is used for inference in other expression contexts but otherwise I think the general idea is nice.

Centril avatar Oct 04 '18 15:10 Centril

I don’t particularly like either Scala or Kotlin-style syntax for Rust, but the Swift-style syntax I could see:

some_operation.and_then($0.get(1));

vec.iter().fold(0, $0 + $1)

The benefit being that it’s completely unambiguous, there is no implicit mapping of occurrences of _ to the order of arguments. It wouldn’t require making it a keyword or implicit parameter.

The use of $ specifically could work, I think. It might be nice because it resembles the (related) concept of macro substitutions already in the language, or confusing for the same reason.

As far as I know, identifiers in Rust cannot start with a number, so there wouldn’t be any ambiguity in the syntax.

tmandry avatar Oct 04 '18 16:10 tmandry

I would prefer something like #0 which seems closer to raw identifier syntax.

mark-i-m avatar Oct 04 '18 16:10 mark-i-m

I donno if the parser can manage ? but it should probably only be used for error handling. it might likely break too much existing code. Could we get away with .1, .2, etc.? so, vec.iter().fold(0, |acc, x| x + acc); becomes vec.iter().fold(0, .1 + .0);? It's kinda ugly though.

burdges avatar Oct 04 '18 16:10 burdges

I'm not a fan of the ambiguity around what would be lazily evaluated, eg.

foo(bar($1 + $2) + $1)

Which of these does it translate to:

foo(|x| bar(|a,b| a+b) + x)
foo(|x,y| bar(x+y) + x)
|x,y| foo(bar(x+y)+x)

I think even if we define rules, it will be confusing to read.

Diggsey avatar Oct 04 '18 16:10 Diggsey

I was actually going to mention Elixir's syntax as well, which is something like

vec.iter().fold(0, &1 + &2)

But with it starting on 1 and using & would cause a lot of issues, so I left it out for the sake of the simpler ones.

ElectricCoffee avatar Oct 04 '18 20:10 ElectricCoffee

You missed some @Diggsey like

foo(|x,y| bar(|a| a+y) + x)
foo(|x| bar(|_,b| x+b) + x)

I'm worried this might create a barrier to polymorphic closures.

burdges avatar Oct 04 '18 20:10 burdges

One could always look at the individual implementations of the other systems and see how they do it.

Maybe that gives some insight to how they got around this mess...

ElectricCoffee avatar Oct 05 '18 06:10 ElectricCoffee

Maybe we should make a new style? Something like

vec.iter().fold(0, .1 + .2);

But actually i'm in love with Scala's closure.

ghost avatar Oct 13 '18 11:10 ghost

Related: https://internals.rust-lang.org/t/simple-partial-application/7685/22?u=scottmcm

I think I'm far more fond of only doing the one-argument-used-once cases, which don't have the "where's the end of the lambda?" problem as much. And since this is just a short form, there's no need to handle everything.

scottmcm avatar Nov 02 '18 01:11 scottmcm

A testimony: in Swift, we've got unnamed (numeric) closure arguments. Even after having spent 4 years with Swift, I find reading others' code with such closure arguments quite puzzling, apart from the most trivial cases like collection.reduce(0) { $0 + $1 }.

It's not that such code is "bad" or even "complicated". But it quickly becomes hard to parse visually. E.g. there can be a closure containing another closure, which is not at all that uncommon – I've been writing a lot of such code for a fintech company that had its business logic implemented as many functions in the form of 2 or 3 short but nested map-reduce like operations. I can tell you, it's not nice figuring out which arguments belong to which closure ($0 is always the argument of the innermost one, so see two $0s even on the same line, and they might not mean the same thing).

And then there's the changeability aspect too. If you want to capture a closure's unnamed argument, you will now need to go back, give it a name, change its use in every place it was used, and only then can you capture it. (Because of this, I mostly just stopped doing it.) That's way more annoying than having to write |x| x.get(1) once. (And while we are at writing: let me immediately counter my own previous argument by saying that I think this proposal is too biased in favor of ease of writing, whereas we tend to spend more time reading code than writing.)

Rust also has the (great) property that overloadable binary operators are available as trait methods, e.g. Add::add. So the (AFAICT very common) case of just needing the behavior of a single binary operator is already well-served by simply passing the trait method instead of creating an unnecessary closure.

H2CO3 avatar Nov 02 '18 22:11 H2CO3

So the (AFAICT very common) case of just needing the behavior of a single binary operator is already well-served by simply passing the trait method instead of creating an unnecessary closure.

I think there's another good hidden point here too: sometimes one needs to do |x, y| x + y to trigger coercions or derefs or similar, so maybe a first step before syntax changes should be more coercions on functions so that one never needs to pass such pointless-seeming closures.

scottmcm avatar Nov 04 '18 23:11 scottmcm

With impl Trait now stable, it should be pretty easy to write combinators such as by_ref or by_deref, something along the lines of:

pub fn by_ref<T, U, F: FnOnce(&T) -> U>(f: F) -> impl FnOnce(T) -> U {
    |x| f(&x)
}

pub fn by_deref<T: Copy, U, F: FnOnce(T) -> U>(f: F) -> impl FnOnce(&T) -> U {
    |x| f(*x)
}

Before adding any coercions, I'd be interested in achieving this using similar functions. Feels more functional, less ad-hoc, and to be honest, less scary. (If a function is lifted from value-land to ref-land or vice versa, I'd very much like to know it.)

H2CO3 avatar Nov 04 '18 23:11 H2CO3

Feels more functional, less ad-hoc, and to be honest, less scary.

Can you elaborate on that feeling? To me it seems like it's weird for

it.map(|x| str::len(x))

to be totally fine when

it.map(str::len)

is a type mismatch error

error[E0631]: type mismatch in function arguments
 --> src/lib.rs:2:8
  |
2 |     it.map(str::len)
  |        ^^^
  |        |
  |        expected signature of `fn(&'a std::string::String) -> _`
  |        found signature of `for<'r> fn(&'r str) -> _`

Since I didn't call anything different between the two things.

Repro: https://play.rust-lang.org/?gist=94d9c13c639ef0611e6bad92d950ab0b

scottmcm avatar Nov 05 '18 00:11 scottmcm

Sorry, that's not what I meant. I thought you were talking about &T -> T and/or T -> &T coercions, but it's apparently just normal Deref coercions applied in a wider context to allow η-equivalence. That would indeed be nice.

H2CO3 avatar Nov 05 '18 00:11 H2CO3

Procedural macros for closures with shorthand argument names (like in Swift): lambda

Example:

Some(3).filter(l!($0 % 2 == 0));

May be useful to someone.

kgv avatar Mar 27 '20 14:03 kgv

Visual clarity of || is kinda important. Kotlin has the { } indicating the presence of a closure, so does swift. I feel current proposals lack such visual indicators.

I know not needing to write out the || is the point, but it helps with reading the code a bunch

nuts-n-bits avatar Sep 05 '24 05:09 nuts-n-bits

On that note, Kotlin's braces-only closure syntax (and its "scope functions" as the best use of it) would enhance the language substantially while solving this issue. They would mean fewer intermediate variables (cleaning the namespace) and make things that work better as functional patterns (mapping, filtering, matching, error handling) much more concise, greatly enhancing readability and usability all-around, and without sacrificing any performance or adding any ambiguity.

The only problem would be a few intermediate variables now being hidden by the compiler, but that's already done with both matching and macros without issue.

Kotlin's got a lot to learn from Rust, but its mixed imperative/functional constructs are probably the best thing I've used in any language, and Rust could really benefit from them.

ByThePowerOfScience avatar Mar 14 '25 00:03 ByThePowerOfScience

https://github.com/SabrinaJewson/implicit-fn.rs

RossSmyth avatar Mar 22 '25 23:03 RossSmyth