Skip to content

Syntax object.method (without parentheses) is unused #1287

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
petrochenkov opened this issue Sep 20, 2015 · 17 comments
Open

Syntax object.method (without parentheses) is unused #1287

petrochenkov opened this issue Sep 20, 2015 · 17 comments
Labels
T-lang Relevant to the language team, which will review and decide on the RFC.

Comments

@petrochenkov
Copy link
Contributor

struct S;

impl S {
    fn f(self) {}
}

fn main() {
    let s = S;
    let f = s.f; // error: attempted to take value of method `f` on type `S` :(
}

It's a valuable piece of syntax and it would be a shame not to put it into action some day.
Any plans/ideas how to do it better?

Several variants I can think about:

  1. A method with bound self argument

    let f = object.method; // returns a closure
    

    is equvalent to

    let f = |args...| { object.method(args...) }
    

    Swift goes this route. C# also allows converting object.method to a delegate, although explicitly.
    This is probably the most intuitive variant, but, frankly, I think it's pretty useless in practice (or at least rarely useful).

  2. A variant of UFCS

    let f = object.method;
    

    is equvalent to

    let f = typeof(object)::method;
    

    or

    let f = typeof(object)::with_adjustments::method;
    

    I'm not sure how useful is this. UFCS with adjustments (ref, deref, unsize etc.) would be pretty useful, it would allow libraries to evolve more freely (see Vec: looks like is_empty and len are not needed rust#26980 for example of what libraries can't do now if they want to keep backward compatibility), but it doesn't strictly need a value (object), only a type (Object). I don't recall any language doing this.

  3. Parentheses elision

    let f = object.method;
    

    is equvalent to

    let f = object.method(); // errors as usual if >0 arguments are required
    

    This is what D does. Parentheses elision greatly reduces symbolic noise in typical code, but can
    probably be confusing sometimes (I don't use D and don't know how confusing it is in practice) and
    some corner cases (what happens if method returns an object implementing Fn?) had to be clarified.


Another useful unused syntax, somewhat opposite to object.method is "field UFCS": Object::field.
It would be really great to make Object::field a projection function fn(Object) -> FieldType:

objects.iter().map(Object::field); // Does the obvious thing

However, ownership, i.e. the choice between fn(Object) -> FieldType, fn(&Object) -> &FieldType and
fn(&mut Object) -> &mut FieldType should be taken into account somehow.

@ftxqxd
Copy link
Contributor

ftxqxd commented Sep 21, 2015

Unfortunately, this would probably be backwards incompatible, as fields can share names with methods:

struct Foo {
    frob: &'static str,
}

impl Foo {
    fn frob(&self) -> &'static str { "bar" }
}

fn main() {
    let foo = Foo { frob: "foo" };
    println!("{}", foo.frob);   // foo
    println!("{}", foo.frob()); // bar
}

@petrochenkov
Copy link
Contributor Author

@P1start
Yeah, I know, but I don't think it's a backward compatibility problem - in case of conflict a field still can be preferred for object.method and a method can be preferred for Object::field, just like now.

@nrc nrc added the T-lang Relevant to the language team, which will review and decide on the RFC. label Aug 22, 2016
@gnzlbg
Copy link
Contributor

gnzlbg commented Jan 26, 2017

@petrochenkov any plans to pursue this?

I've wanted this way too often and might fit in the 2017 roadmap of improving ergonomics.

@oli-obk mentioned in IRC: "just prefer object.field over object.method if both field and method have the same name"

@petrochenkov
Copy link
Contributor Author

@gnzlbg

I've wanted this way too often

What exactly - variant 1, variant 2, variant 3 of object.method, or Object::field or both?

@oli-obk
Copy link
Contributor

oli-obk commented Jan 26, 2017

My vote is for variant 1, the others are confusing. Static method lookup should only occur with types and modules, not with bindings.

@gnzlbg
Copy link
Contributor

gnzlbg commented Jan 26, 2017

@petrochenkov variant 1. Simple things should be simple. One can just pass a function as an argument, the same should be easily possible for object methods. Having to create a closure is an inconvenience when the compiler can do it for you. No need to think about move || vs || nor the number of arguments of the closure. When this is required often, the current best solution is to create a let binding to a closure in the enclosing scope. We can do better.

@petrochenkov
Copy link
Contributor Author

@gnzlbg @oli-obk
Could you write an RFC draft then (with motivation, etc)?
I'm really bad (and slow) at writing texts, but I can do a technical review before it's submitted.

@strega-nil
Copy link

I'd like to express my support for variant 1; seems like a nice bit of sugar :)

@gnzlbg
Copy link
Contributor

gnzlbg commented Jan 27, 2017

So I wrote a [Pre-RFC] Unify bindings to callables in internals. In a nutshell it says that a binding to a callable always return a closure, and when the callable is of the form object.method, that closure has an environment that captures the object. This basically makes bindings to all callables work "as one wold expect", that is, the binding accepts the same arguments that the original callable would have accepted if it were used instead.

The main difference with variant 1, is that it also adds the possibility of binding associated functions using object.associated_function that desugars into an environment-less closure |args...| Deduced::associated_function(args...).

(note: since closures without environment work as function pointers, everything should just work)

EDIT: i've removed the associated function syntax from the RFC.

@nagisa
Copy link
Member

nagisa commented Jan 27, 2017

My vote is against them all :)

  1. Borrows or moves object despite fn(*) -> * being a Copy otherwise. Very counter-intuitive.
  2. Why not… just use UFCS? There’re also ambiguity issues with typeof(object)::method in generic contexts.
  3. Ewww.

@petrochenkov
Copy link
Contributor Author

@nagisa

Why not… just use UFCS?

UFCS doesn't do adjustments.
You can't turn |vec| vec.slice_method() into Vec::slice_method because Vec itself doesn't have slice_method, it's only accessible through deref.

I agree though, object.method is not the best syntax for "UFCS with adjustments".

@gnzlbg
Copy link
Contributor

gnzlbg commented Jan 27, 2017

@nagisa

Borrows or moves object despite fn(*) -> * being a Copy otherwise. Very counter-intuitive.

How do you create a callable to an object's method, that takes the same arguments as invoking the method through the object (object.method(args...)), without borrowing the object, to obtain a Copy fn(*) -> *? You could probably use an unsafe raw pointer for that, but... that's unsafe.

@nagisa
Copy link
Member

nagisa commented Jan 27, 2017

@gnzlbg I suppose our expectations for what this ought to behave like differ. I feel like object.method making a binding is significantly different from what similar syntax already does. struct A { f: fn(Self, ...) -> ... }; let a = A { ... }; a.f for example will not cause a borrow or move to happen, because a.f is a plain function pointer that’s Copyable.

Option 1 is basically a proposal to add a different context-sensitive (something that would “activate” if there was impl A { fn f(self, ...) -> ... { ... } and no field with same name) meaning to a.f which returns something resembling a function pointer, but not quite, while also borrowing or moving the a.

@Nemo157
Copy link
Member

Nemo157 commented Jan 27, 2017

@nagisa that's similar to how calling those two functions look though, to call the fn stored in a field you need (a.f)(a, ...) (which I'm not sure is even possible, EDIT: yes it is: https://is.gd/SVCaZh), but method calls "magically" allow you to omit the self parameter and automatically borrow/move as needed allowing a.f(...) to work.

@gnzlbg
Copy link
Contributor

gnzlbg commented Jan 27, 2017

struct A { f: fn(Self, ...) -> ... }; let a = A { ... }; a.f for example will not cause a borrow or move to happen, because a.f is a plain function pointer that’s Copyable.

Why is f potentially being a function pointer relevant? For example: struct A { f: i32 }; let a = A { ... }; a.f won't cause a borrow of a either, but that is because f is a field (independently of what the type of f is).

@nagisa is the point that you are trying to make that object.identifier would sometimes mean accessing a field (which never borrows object) and sometimes mean create a callable for an objects method (which consumes or borrows object)? If so, are you mainly concerned about the complexity this adds to the language, or do you think this also introduces some pitfalls? (as far as I can see the type system and the borrowck would prevent all possible pitfalls from compiling but I haven't thought that much about it)

Option 1 is basically a proposal to add a different context-sensitive (something that would “activate” if there was impl A { fn f(self, ...) -> ... { ... } and no field with same name) meaning to a.f which returns something resembling a function pointer, but not quite, while also borrowing or moving the a.

Kind of. It would return a closure, that would statically dispatch the method on a value of the object or a reference to it. No dynamic dispatch involved (because each closure is its own unique type and can perform static dispatch, as opposed to function pointers which type-erase the actual function involved), and no function pointer involved since only environment-less closures can decay to function pointers but these closures will always have an environment ( references can be involved, and technically "references are pointers", but this is the case with any method call in every situation).

@oli-obk
Copy link
Contributor

oli-obk commented Jan 27, 2017

Is solving all ambiguity problems by changing the operator out of the question?

  • object -> method is probably a nogo, even though I like it
  • closure object.method could be a new keyword that only applies if followed by $ident.$ident
  • object::method because this operation is mostly static, but processes a runtime value
  • |...| self.method(...) being a new closure syntax that copies all arguments to wherever ... is mentioned in the closure body

@gnzlbg
Copy link
Contributor

gnzlbg commented Jan 27, 2017

out of the question?

Nothing is out of the question :) From the alternatives you provide I still like the let f = object.fn; syntax more because it is more symmetric to the free function binding syntax let ff = free_fn; and I'd rather not introduce a completely new third syntax for binding functions.

Having said this |...| self.method(...) might interact with variadics, so I don't think it could be possible to resolve whether it is good or bad till there is at least a solid path forward for variadics in Rust.

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

8 participants