Naming a method to create a "member function" closure

It's very useful that the bare name of a function can be used as a value of the appropriate function type, e.g.

usize::from_str("1234").map(usize::is_power_of_two) // Result<bool, _>

This pattern breaks down somewhat when it comes to methods. While type::method beheves like a function, object.method does not - the compiler will insist we need to call it first. However, in the IMO morally equivalent case that method is a field of function type, calling that function is done by saying (object.field)(), exactly what you'd expect if object.field is actually naming a function.

Of course, a function-typed field and a method call are not semantically equivalent, because only the latter case passes self as a parameter. There is nonetheless an intuition, most easily seen in C++'s notion of pointer-to-member, that object.method nonetheless names a function equivalent to method but with object already bound as the first parameter, i.e. currying the first parameter into the function. To do the same in Rust, I'm effectively suggesting sugar such that this, used as a value(*),

object.method

is equivalent to

|...| Type::method(object, ...)

with all of the existing rules about whether the resulting closure is clone/copyable, what it's capturing and for what lifetime, etc, still applying. I'm not suggesting currying (as happening implicitly or not) be codified as a language feature in any other context. The only reason this can't just be a macro is because there's no way for a macro to determine the arity of the closure it'd need to produce and the type needed.

A related construction is the Scala feature of "placeholder parameters" where one can write _ + _ and implicitly produce a closure |_1, _2| _1 + _2. This would be a generalisation of what I'm suggesting, but if anyone's suggested something similar in Rust, I expect this suggestion would be an easy extension of that.

I don't think this creates any backwards compatibility concerns, since you're plain not allowed to do this ATM, but I can appreciate it might strike some people as too implicit. Thanks for any feedback you have

(*) I've not looked into the formal grammar, so I don't know if there's any distinctions that need to be worked around because (|a,b| Type::method(object, a, b))(foo, bar) is not actually equivalent to, object.method(foo, bar)

This has surely come up before, but I don't know of any particular previous writeup. Here are some things that would have to be decided about the semantics:

  • Is [object.method, object.method] legal? That is, do all occurrences of the bound-method expression for the same receiver type have the same type (unlike closures)? If so, that's a new thing in the language semantics, not just sugar for a closure. If not, then I think it would be surprising to users, because object.method doesn't look like it's declaring something new.
  • Is there a way to make the implicit closure a move closure?

I think I’ve seen this before, though I won’t do the research as to where that might have been… possibly on this forum though.

I believe one problem is that at the moment, fields and methods are allowed to have the same name, and this isn’t even all that uncommon, especially with private fields. To showcase just one example, here.

5 Likes

I believe one problem is that at the moment, fields and methods are allowed to have the same name,

Which is why Java had to invent the new syntax receiver::method for its lambda shorthands (Java didn't use :: at all before that).

2 Likes

I think it depends if you want equivalent of move closures to exist. Without that, it'd be a simple case that can have always the same type, because it wouldn't be capturing arbitrary environment, but specifically the method's self argument, which has its type already defined in the method's signature.

1 Like

Point of comparison: Swift. Swift has this feature and I wish it didn’t, but the reasons why I don’t think it works well in Swift don’t apply to Rust:

  • Swift has name-based overloading for methods based on argument labels, if present (required rather than optional like C# or Python); the “binding” syntax isn’t consistent about requiring labels. Rust doesn’t have name-based overloading (and per the previous very long discussion thread, I don’t think it should, even if it eventually supports Python-like argument labels and default arguments).

  • Swift has to treat all such captures as move (or “cloning”, really, given Swift use cases) because it doesn’t have generalized lifetimes. With its built-in reference-counted class instances, this can lead to accidental retain cycles. In Rust, the capture can be a borrow or a move/copy as appropriate. (Or it could always be a move, and reference syntax could be used to opt-out. The point is the language can handle the lifetimes for either case.)

1 Like

This is already valid syntax:

struct HasFn {
    f: fn(u32) -> u32,
}

impl HasFn {
    fn f(&self) {}
}

fn tform(x: u32) -> u32 {
    x + 1
}

fn main() {
    let l = [1, 2, 3];
    let h = HasFn { f: tform };
    for v in l.map(h.f) {
        println!("{}", v);
    }
}
2 Likes

If we had currying as a language/library feature, this could be a special case of it (currying the self argument)

2 Likes

I'm not entirely sure what a move closure would actually entail here - whether or not you're capturing the only environment value (the self) is defined by the method receiver, what else is left that you'd need to apply move to?

@kpreid

I'd imagine not. Although it'd be counterintuitive, I don't think it's any more so than syntatically equal closures having distinct types in general, i.e. [|x| x, |x| x] fails for the same reason. It's also a pattern continued from different const use sites being distint, albeit more in your face. It's also the more conservative direction - if there's ever a good approach to being able to unify the types of closures, we leave the door open to allowing it in the future.

@idanarye

Partial application and currying would certainly be nice to have in some situations IMO, and isn't a huge change since we already have closures (unlike, say, C or early Java) but I didn't suggest it because I expect that most people would find it too implicit. It's also unclear to me how much value you get using it in a stdlib that's not been designed with it in mind from the beginning, unlike, e.g. haskell.

move would capture self by move even if the function being curried (uncurried?) takes self by reference. Without move the capture would always match the declaration of self in the original function.

2 Likes

With nightly features it is possible to implement this for your own types (with some significant caveats).

  1. Define the type as a union Foo { method: FooMethod, another: FooAnother, ... }
  2. Define the field types as transparent wrappers for struct FooInner {/*data fields*/}
  3. Implement the closure traits for (references to) the method field types, as appropriate.

With that you can get something like this working:

let fizzer = Thing::new(3, "Fizz");

(1..=100)
    .map(&*fizzer.test)
    .for_each( ... ));

Problems include:

  • Accessing union fields is unsafe, even though this type could opt in to safe field access.
  • Non-Copy union fields must be wrapped in ManuallyDrop, so accessing looks like &*object.method
  • The Fn-traits are unstable: tracking issue #29625
  • And probably a bunch of others you'll quickly find if you try to do this for anything practical...

playground demo

At least with this hack the answer is clear: The callable field does have a single named type. Also, I don't think that would be anything new. A closure-returning function fn foo() -> impl Fn(Args) -> Ret only has one return type (for any given selection of foo's generic parameters), and you can e.g. collect those in a Vec.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.