Skip to content
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

Rework generic paths for associated functions: T::function() #6894

Closed
brendanzab opened this issue Jun 2, 2013 · 40 comments
Closed

Rework generic paths for associated functions: T::function() #6894

brendanzab opened this issue Jun 2, 2013 · 40 comments
Labels
P-medium Medium priority

Comments

@brendanzab
Copy link
Member

This issue formalizes @pcwalton's RFC on the mailing list. This was also discussed in the 2013-05-14 meeting and in #6087. Steps towards this have already been made in #6462.

Whilst this is not a high priority change at the moment (understandably there are far bigger fish to fry), this is a semi-common question on the IRC and I think it's good if we have some place to redirect questions to.

Instead of the current:

Real::pi::<float>()

this change would allow you to do:

float::pi()

or

type T = float;
T::pi()

This could also allow for neat things like a generic SizeOf trait: T::size_of(), which is far nicer than the current size_of::<T>(). Or maybe there could be an Init trait for default initializers: T::init().

@brendanzab
Copy link
Member Author

See also: #5635

@brendanzab
Copy link
Member Author

@brendanzab
Copy link
Member Author

Something that needs consideration would be how to handle more complex types. Assuming there was a SizeOf trait, an example would be: (~Option<float>)::size_of()?

@nikomatsakis
Copy link
Contributor

The approach to invoking size_of as described in @pnkfelix's blog post would be:

use impl foo = SizeOf for ~Option<float>;
...
foo::size_of();

@brendanzab
Copy link
Member Author

@nikomatsakis Ah, I didn't realize it was another case of the use impl technique that has been mentioned before.

@pnkfelix
Copy link
Member

pnkfelix commented Jun 2, 2013

I have been wanting to work on this, but it may be too big a task for me to take on immediately. If I decide that I can indeed tackle it, I will assign this issue to myself.

@brendanzab
Copy link
Member Author

@pnkfelix Unfortunately I was having a little trouble following your blog post as the issue is pretty involved (that's no slight against it though). Unfortunately I come from a position of ignorance, without a full understanding of the moving pieces involved.

Which approach do you favour? Personally, from an ignorant user perspective, I think something like (~Option<float>)::size_of() would be highly intuitive, but in an in-depth discussion with @nikomatsakis yesterday on IRC, apparently there are key logical reasons why that can't be done.

At least we can agree that the current way of doing things must be improved (eg. #6898).

@brendanzab
Copy link
Member Author

@pnkfelix for reference, here is the log of our discussion: https://botbot.me/mozilla/rust/msg/3557231/

@nikomatsakis
Copy link
Contributor

@bjz I didn't meant to give the impression it can't be done... just that there are situations that syntax can't express, and other considerations as well. I tried to explain those considerations as well as I could in these blog posts

http://www.smallcultfollowing.com/babysteps/blog/2013/04/02/associated-items/
http://www.smallcultfollowing.com/babysteps/blog/2013/04/03/associated-items-continued/

@pnkfelix
Copy link
Member

pnkfelix commented Jun 3, 2013

@bjz The context of the IRC log doesn't tell me how deeply you all got into this, so forgive me if I cover ground you've already covered. Also, I'm working mostly from memory here, and I may well get some of the details wrong.

Syntax like (~Option<float>)::size_of() turns our notion of a path from a sequence of identifiers into a tree structure (because the tree-structured type-expressions will appear as sub-components of a path), and I'm very hesitant to do that in the short term: it would be a big change to a core component. (This is discussed in my blog post under the heading "Rust type expressions do not naturally fit into Rust path expressions.")

So that's why my proposal factors out ~Option<float> to a identifier, a token that can be referenced in the existing path syntax. That binding is expressed by the use impl item.

A goal I was trying to achieve in my proposal was "minimize changes to the core of the syntax, and hopefully the compiler too." My usual justification for that sort of goal when it comes to adding features like this goes something like "Longer term, after we gather experience with an implementation that can express these general associated items (even if it requires some local syntactic gymnastics), we'll be in a better place to evaluate more expressive options."

But a problem with the above justification is that if the syntax is poorly chosen now, then that may adversely affect the body of code that uses associated items that would be used as the basis for coming up with a better syntax. Because of this problem, I plan to consider any reasonable suggestions for alternative syntax that are offered up.

For example, if you can present an argument for why tree-structured path syntax will pay for itself, or is inevitable, then maybe that would help us reconsider whether (~Option<float>)::size_of() is viable. But for right now, I do not think it is sufficient to say that the syntax would be highly intuitive, because it does not account for how doing this might complicate (or adversely interact with) other portions of the syntax and/or compiler, which may inject more confusion and non-intuitive behavior.

More concretely: You may need to even incorporate issues like how the macro-expander works. I do not know off-hand whether our term-rewriting macro_rules! can deconstruct and/or introduce new paths, or if paths are treated as atomic entities in that context. But either way, a change to the path syntax will affect either the definition of such macros or how one uses them.

@pnkfelix
Copy link
Member

pnkfelix commented Jun 4, 2013

But either way, a change to the path syntax will affect either the definition of such macros or how one uses them.

This may have been an over-statement on my part. (There may be a way to generalize the path syntax to allow tree structure that does not break old macro definitions.) But I am not certain either way, and in any case I think there would be push-back to putting tree structure into paths.

@brendanzab
Copy link
Member Author

@pnkfelix Thanks for the responses! Could you re-explain what you mean by 'tree structure in paths'? And why is this considered undesirable?

@brendanzab
Copy link
Member Author

I guess one of the other issues I have with the current way of accessing associated functions via the trait is the asymmetry with method calls. That is, 2f.sqrt() or "hiya".len() is accessed with respect to the implementing type, where Real::pi() or Zero::zero() is not. I do however understand that those are trivial examples, and there are far more complex edge cases to consider.

@brendanzab
Copy link
Member Author

@pnkfelix Sorry for the multi-posting, but I just had a thought. If we go with what @pcwalton initially described on the mailing list and my original issue description, what would the difference be between:

use impl OwnedOptFloat = SizeOf for ~Option<float>;
...
OwnedOptFloat::size_of();

...and:

type OwnedOptFloat = ~Option<float>;
...
OwnedOptFloat::size_of();

...other than the specification of the specific trait? Also, why would you have to specify the specific trait impl when you don't have to do that for methods?

@pcwalton
Copy link
Contributor

pcwalton commented Jun 6, 2013

OwnedOptFloat::size_of() would be the OO solution instead of the FP solution, using the terminology here: http://smallcultfollowing.com/babysteps/blog/2013/04/03/associated-items-continued/

@brendanzab
Copy link
Member Author

From IRC:

pcwalton:
I feel like� explaining ":: has this behavior whereby it looks through all traits in scope and picks the one with a method with that name" is a wtfrust.com moment
there are also other options that seem less magical
(Range for uint)::range() for example
range::<for uint>()

But why the asymmetry with method calls? If folks are capable of dealing with this for with methods, why not for associated items as well? Why aren't we pushing for ([1, 2, 3] for ImmutableVector<int>).map(|&x| x * x) or [1, 2, 3].map::<for int>(|&x| x * x) as well?

@pnkfelix
Copy link
Member

pnkfelix commented Jun 6, 2013

@bjz paths right now are sequences of identifiers: a::b::c::d. It is isomorphic to a list (of identifiers).

An expression tree has richer structure. Instead of looking like a linear sequence, there is instead some amount of recursive nesting: The path has type expressions inside of it, which in turn can have more paths inside of them, etc.

Right now, the grammar for Rust keeps paths segregated from the grammar for types as well as other expressions; I am trying to maintain that segregation, if only to keep the amount of change to the language low.

@pnkfelix
Copy link
Member

pnkfelix commented Jun 6, 2013

@bjz The specification of the specific trait is the difference between the two options you presented. It makes the method selection unambiguous, regardless of what other traits are implemented by ~Option<float>.

@brendanzab
Copy link
Member Author

Right now, the grammar for Rust keeps paths segregated from the grammar for types as well as other expressions; I am trying to maintain that segregation, if only to keep the amount of change to the language low.

Just curious, why aren't we using the . operator for associated items? Has that been considered? (I'm sure it probably has :P)

The specification of the specific trait is the difference between the two options you presented. It makes the method selection unambiguous, regardless of what other traits are implemented by ~Option.

I just worry about things like this: Zero::zero::<int>(). Surely there is something we can do to reduce the verbosity? Even something like (Zero for int)::zero() is concerning. Isn't type inference meant to help alleviate this repetition?

@pnkfelix
Copy link
Member

pnkfelix commented Jun 6, 2013

@bjz I don't have an immediate response to your point about the asymmetry with method calls, apart from: "I am more concerned about ensuring that new features I introduce do not have expressiveness bugs." I'll think more on the asymmetry question.

@brendanzab
Copy link
Member Author

@pnkfelix Definitely, that is a very valid concern. I would hate to be the one responsible for pushing something that we'd later regret (and be cursed for years to come).

@pnkfelix
Copy link
Member

pnkfelix commented Jun 6, 2013

@bjz Regarding your Zero example, I would think in the common case that the type in question would not be int, but rather a type parameter with a single bound: fn<X:Zero>(x:X) { ... }; item 8 in the proposal from my blog post says that single bounds are expanded into the named form, so that you can just write X::zero in that case.

Otherwise, I was planning to live with putting in the use impl declaration. But I have not written much code of that form yet.

@brendanzab
Copy link
Member Author

@pcwalton reread the blog post. I agree that Self::(Add<Rhs>::Sum) for the OO-style is unfortunate. But how often would one actually do that, apart from actually writing generic APIs? I agree there is a tradeoff to be had.

@nikomatsakis
Copy link
Contributor

@pnkfelix one thing that has been bugging me about this is that I believe that these named "type+trait" pairs must inhabit the same namespace as types, so having <X:Zero> be "equivalent" to <X:X=Zero> implies that they such names do not shadow but rather "co-exist"? It makes me wonder in that case if we couldn't just say that X::foo would search for an item named foo among the bounds of X, complaining if there was a duplicate (that is, if there were two bounds with associated items of that name). This way, only in the case where a type parameter had multiple bounds that both offered an associated item foo would you need an explicit name. Moreover, there is still no "type-based" OO resolution happening, not really. This doesn't address @bjz's concerns at all, of course, since it applies only to type parameters. Just something I was thinking about.

@brendanzab
Copy link
Member Author

It makes me wonder in that case if we couldn't just say that X::foo would search for an item named foo among the bounds of X, complaining if there was a duplicate

To clarify, are you saying that X would be the Self type? If that is the case, wouldn't that mirror what we already do for the . operator? ie. complain if there is a duplicate item associated with the Self type?

Re-reading the Associated Items Continued post, I'm pretty sure the OO-syntax pretty much what I am proposing. Again, that approach has it's drawbacks, but it seems to be relegated to the library/api author. And as a library/api author, I know that we're more than happy to jump through hoops to craft a beautiful interface for our users.

@pnkfelix
Copy link
Member

pnkfelix commented Jun 8, 2013

@nikomatsakis Back when I wrote the proposal, I thought the two would be in different namespaces (and thus they do not shadow, but rather co-exist naturally).

In particular, my thinking was that the namespace for the leading components of a path would be distinct from the namespace for a final component of a path. Reflecting on that now, I guess the two namespaces would not be disjoint (since I think the names of modules would need to be in both places).

But maybe that is not workable. I will need to review.

@pnkfelix
Copy link
Member

@nikomatsakis A question, and a response to a long-standing point you made.

  • Is there actually any soundness issues you anticipate if the name for a type+trait pair occupys the same namespace as types? I.e. go so far as to even allow the code to use it in place of a type in a item declaration, or an instantiation of a parametric type? (I do not know why I was trying so hard to avoid this outcome.)
  • This:

" if we couldn't just say that X::foo would search for an item named foo among the bounds of X, complaining if there was a duplicate (that is, if there were two bounds with associated items of that name). This way, only in the case where a type parameter had multiple bounds that both offered an associated item foo would you need an explicit name."

sounds fine to me, as long as (1.) we're okay with the outcome that adding a new associated item to a trait could force other client code to become invalidated, and (2.) no one complains if I write code that uses the explicit names "unnecessarily" in the presence of such a rule. :)

@nikomatsakis
Copy link
Contributor

@pnkfelix I don't see why there'd be any soundness issues to allowing the name to be used in place of a type, it'd just be a ... complication. It seems to me like the name for the type+trait pair would effectively be an alias for the type in a context where the trait wasn't needed?

@pnkfelix
Copy link
Member

@nikomatsakis right. I just see it as an attempt to make sensible the rewriting: f<T:A> => f<T:T=A>. But maybe that doesn't actually matter; maybe the linguistic form f<T:A> should simply be handled distinctly from the form f<T:I=A> in the compiler; I just thought it would nice to desugar the former into the latter.

@nikomatsakis
Copy link
Contributor

@pnkfelix Yes, I see the motivation. I am somewhat indifferent though making the default more special, in that it searches all the bounds, seems potentially nicer in practice, since it means that f<T:A+B> would "just work" so long as the method names of A and B are disjoint.

@brendanzab
Copy link
Member Author

benh on IRC:

I think my favorite approach would be to say Type::trait_member in the case where its unambiguous and allow Trait::<for Type>::trait_member as a "fully explicit" form, and then allow use I = Trait for Type; I::trait_member as a shortcut for repeatedly resolving things from the trait. So basically "all of the above"

Just a thought. I like it. That said, we don't want to make things complex just to please a potential minority (me?), and end up making life difficult in the long run for everyone.

@pnkfelix
Copy link
Member

pnkfelix commented Sep 8, 2013

Just one note: I don't mind the combination of approaches outlined by benh (as quoted by bjz), that combination still overlooks one important use case: I (and perhaps we) want to be able to introduce the I bindings (as in I = Trait for Type) within a function signature.

That is, we want to be able to define a polymorphic function f that has type parameter X with trait bounds S and T, and be able to refer to the associated items of X in the type signature of f itself. This is the motivation for the f<X: I=T + J=U>(x: I::some_type, y: J::some_type) general syntax that I described in a blog post on this topic. (Which would then have simpler shorthands when unambiguous.)

I had to go review the blog post to remind myself why the proposed combination would not be 100% satisfactory to me, so I thought a reminder here would be justified.

Having said that, I do not object to adding the features proposed in benh/bjz's note. And the X: R=T syntax can certainly wait until we have full-fledged associated items; the syntax is unnecessary for our current status where the only kind of "associated items" are trait methods.

@sanxiyn
Copy link
Member

sanxiyn commented Oct 24, 2013

Tagging RFC.

@emberian
Copy link
Member

Nominating for backwards compat.

@alexcrichton
Copy link
Member

I'm not sure that we should do this for type aliases (type foo = bar), but I would very much like this done for types in general (Type::static_trait_function())

@pnkfelix
Copy link
Member

@alexcrichton In the long term, if we support A::static_trait_func() for a type parameter A: Trait, then it seems natural that we would also support it for a type alias foo::static_func() where type foo = bar; But that's just me talking about the long term; I don't have much of a stake in what we do for the short term, as long as the long term plan is backwards-compatible.

@alexcrichton
Copy link
Member

If the implementation "Just works" such that type aliases work, then I'm all for it! I certainly don't want to explicitly forbid them if they happen to work.

@pnkfelix
Copy link
Member

Accepted for P-high.

@nrc
Copy link
Member

nrc commented Feb 18, 2014

cc me

@rust-highfive
Copy link
Collaborator

This issue has been moved to the RFCs repo: rust-lang/rfcs#288

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
P-medium Medium priority
Projects
None yet
Development

No branches or pull requests

9 participants