Skip to content

Implicit closure parameters #2554

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
ElectricCoffee opened this issue Oct 1, 2018 · 21 comments
Open

Implicit closure parameters #2554

ElectricCoffee opened this issue Oct 1, 2018 · 21 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@ElectricCoffee
Copy link

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.

@leonardo-m
Copy link

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

@mark-i-m
Copy link
Member

mark-i-m commented Oct 1, 2018

Seems related to #1577

@Centril
Copy link
Contributor

Centril commented Oct 4, 2018

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.

@tmandry
Copy link
Member

tmandry commented Oct 4, 2018

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.

@mark-i-m
Copy link
Member

mark-i-m commented Oct 4, 2018

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

@burdges
Copy link

burdges commented Oct 4, 2018

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.

@Diggsey
Copy link
Contributor

Diggsey commented Oct 4, 2018

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.

@ElectricCoffee
Copy link
Author

ElectricCoffee commented Oct 4, 2018

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.

@burdges
Copy link

burdges commented Oct 4, 2018

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.

@ElectricCoffee
Copy link
Author

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...

@ghost
Copy link

ghost commented Oct 13, 2018

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.

@Centril Centril added the T-lang Relevant to the language team, which will review and decide on the RFC. label Oct 15, 2018
@scottmcm
Copy link
Member

scottmcm commented Nov 2, 2018

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.

@H2CO3
Copy link

H2CO3 commented Nov 2, 2018

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.

@scottmcm
Copy link
Member

scottmcm commented Nov 4, 2018

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.

@H2CO3
Copy link

H2CO3 commented Nov 4, 2018

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.)

@scottmcm
Copy link
Member

scottmcm commented Nov 5, 2018

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

@H2CO3
Copy link

H2CO3 commented Nov 5, 2018

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.

@kgv
Copy link

kgv commented Mar 27, 2020

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.

@nuts-n-bits
Copy link

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

@ByThePowerOfScience
Copy link

ByThePowerOfScience commented Mar 14, 2025

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.

@RossSmyth
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.
Projects
None yet
Development

No branches or pull requests