Idea: allow methods with provided `self` to be used as closures

From time to time I encounter the following papercut with closures:

foo.map(|val| { bar.zoo(val) })

I really want to write it as just foo.map(bar.zoo) and, on the first glance, it does not look like implementing it has any fundamental obstacles.

By tweaking how the compiler processes methods with provided self, the following code could also work:

fn f(bar: &Bar) -> impl Fn(u8) -> u32 {
    // Currently we have to write `|val| b.foo(val)`
    bar.foo
}

UPD: It could be done by applying the following desugaring to the method syntax: foo.bar -> |arg1, arg2, ..| Bar::bar(foo, arg1, arg2, ...) (or Foo::bar for inherent methods).

What do you think?

2 Likes

One thing that jumps to mind is that bar.zoo might be a field.

(AKA the opposite side of the "why did I have to write (bar.zoo)() to call a fn in a field?" question.)

Also there's the question of what bar.into should do, since we don't have generic closures (yet).

8 Likes

I am not sure I understand. The compiler would handle fields and methods separately as it does today. In other words, this code should work:

struct Foo {
    f1: fn(u8) -> u8,
}

impl Foo {
    fn f2(&self, val: u8) -> u8 { (self.f1)(val + 1) }
}

let f = Foo { ... };
let val: Option<u8> = ...;
let res = val.map(f.f1).map(f.f2);

Why can't I follow the simple desugaring rule f.foo -> |arg1, arg2, ..| f.foo(arg1, arg2, ...)?

It seems clear what it should do, but yes, that would have the same problem that impl-Trait-in-closures has. It should produce something of a particular type determined by the context it's used in. So, for instance, u32::from is a function from T -> u32 where u32: From<T>, and could be passed to something expecting a function from u8 -> u32, or something expecting a generic function.

1 Like
struct S {
    field: u32,
}

impl S {
    fn field(&self) {
        println!("{}", self.field);
    }
}

fn main() {
    let s = S { field: 42 };
    println!("{}", s.field);
    s.field();
}

Fields and methods are different namespaces and can use the same names.

You can, somewhat. |arg| f.method(arg) can't be generic, though; it has to have a specific argument type. And the type system may or may not be able to figure out the type, depending on the context; you might have to write |arg: SpecificType| f.method(arg).

3 Likes

Hm, yes, it's a problem. One potential solution could be to make fields a higher-priority items (i.e. the method desugaring will not apply if there is a public field with the same name) to preserve the existing behavior and add a (Clippy?) lint which would warn against name collisions.

Yes, and it's fine to raise a compilation error in the latter case as we would happen today if we are to try to write the desugared code.

Bringing in another paint can: Java (plus Kotlin?) uses bar::zoo as the syntax for this. I believe that’s also not quite compatible in Rust because values and types can have the same name (I’ve seen people do it to fake contents-less structs being their own constructor), but it might be less breaking in practice (as newpavlov says, perhaps the existing meaning can be preferred when there’s a conflict).

2 Likes

The usual Rust solution is that whenever there is an ambiguity, compilation fails asking the user to disambiguate. This is what is used for method resolution (if traits have same method name, and both are in scope, you need to disambiguate at call site)

So maybe in a new Rust edition we could have Rust infer whether x.something refers to a field or a name, and fail loudly when it could be both (current Rust behavior is error inducing anyway)

2 Likes

@josh's example compiles. Current Rust behavior would be broken.[1] Still doable over an edition, though... given a way to disambiguate.


  1. And if you meant "could be both" via some sort of trait bound check or such, it's still pretty easy to give examples which compile today which would be ambiguous with that change. ↩︎

I'm against forcing a disambiguation on accessing a field with the same name as a method. I pretty regularly write code with a getter function with the same name as the field it gets, like:

pub struct Foo {
    size: usize,
    ..
}
impl Foo {
    pub fn size(&self) -> usize {
        self.size
    }
    .. // Other methods that might read and/or update the `size` field directly
}

And it would be a pain if every time I interact with size needed some fancy syntax just because I wanted a publicly-visible accessor method.

2 Likes

What about this: if the type of the size field does not impl Fn* traits, then x.size() is resolved as the method (since if it were the field, it wouldn't type check). Otherwise, it's ambiguous.

We’re discussing adding a syntax where the method is not invoked upon reference, but instead curried. So you can’t use invocation parens to disambiguate.

4 Likes

what about writing it as use_fn(my_self.method(...)) so it's clear you're using the method and not the field, but you still don't have to write out the full lambda syntax

Why is this a significant enough papercut to be an issue? The code with the lambda syntax is readable to me. And code is read more often than it is written.

It would also be yet another language feature for new people to learn. Rust has a lot of those. I don't mind that much myself (my background is C++ after all :sweat_smile:) but I'm not sure this feature carries it's weight.

I would rather spend the complexity budget on things that expand where and how the language can be used than on syntax sugar.

4 Likes

That looks like a call, rather than an un-called method.

I think use_fn(self.method) looks clear and obvious, if we can make it work. If we have to add any syntax on top of that, we have to weigh that against |arg| self.method(arg).

1 Like

The extra |arg| is mildly annoying, but I'm not sure if this change will make enough difference.

  1. map(func) tends to have worse type inference than map(|arg| func(arg)), because it's stricter about lifetimes, and doesn't do type coercions.

  2. There are other cases map(|arg| self.func(arg.field)), map(|arg| self.func(arg, 2)), or map(|arg1, _| self.func(arg1)), and there's always going to be a jump from the abbreviated form to the more verbose form. I'm not sure if fixing one case solves enough of the problem.

There are different approaches, e.g. anonymous arguments: self.func($0) inside a closure to avoid naming args. There could be currying, including currying of self (JS has .bind(self) on functions, although that would need syntax sugar itself to be worth using).

7 Likes

This is effectively proposing to perform trait solving in name resolution, which seems overkill and likely to lead to issues in the future.

2 Likes

Maybe syntax could be used such as: bar.::baz? This makes sure that there is no ambiguity since colons cannot be used in identifiers.

We've talked about using syntax like value.Trait::method(...) to disambiguate calling a method from a trait. Given such a disambiguator, value.::method would be confusing, as it looks like using that same disambiguator to refer to a global method.

(Also, it's adding more syntax, which makes the appeal of the shorthand it enables less appealing.)