-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Associated field inheritance #250
Conversation
I'll admit I cannot tell if the details perfectly fit with each other or not, and if there are unconsidered corner cases or not. But this proposal feels right. All the components can be handy even if we do not use them for inheritance. Actually, even if we eventually go with the other inheritance proposals, I can still see people proposing these features separately anyway, and may accidentally create multiple ways to do inheritance in Rust, which is undesirable. So why not consider them as a whole and use them for inheritance? Even if we decide to reject this, at least we will know which of the components we can adopt, and which we cannot. Win. In my eyes, this proposal makes traits somewhat more like Scala's, and in a good way. Field mapping is a clever way to avoid possible name conflicts between the implemented traits, and more importantly, make the name choices local and explicit to the implementer. One thing that I don't like about Scala's traits is how their orders in the declaration affect name resolution. And Rust resolves this by always requiring the programmer to explicitly state his/her intention, even go as far as disabling methods if the trait is not in scope. It may seem verbose at times, but I love it. There is less "global state" to track. And to me, field mapping is just following this fine tradition, if a language that hasn't even hit 1.0 can be said to have a tradition, that is. ;) |
} | ||
|
||
struct MyTuple(uint); | ||
impl Foo for MyStruct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
impl Foo for MyTuple
?
Associated fields seem nice but I'd like to mention one point where data hiding could be improved: Users of a trait, as well as default impls, might use fields as if they were plain values. But it's impossible for a trait to know if an implementation needs dynamic behaviour for access or storage of a property. Therefore associated fields could/should be mappable to getters and setters. It's not just an ergonomic issue. The trait is an abstract description of a type - it wouldn't know requirements of implementations. Some uses of accessor functions are: hide internal representation, specialised representation, serialisation, lazy loading, lazy evaluation. Of course, there's one glaring unanswered question: can you take the address of such a field? |
@arcto: I think associated fields is for one use case and one use case only: where we need to access common bare fields without any magic. If we need accessor functions, the Rust way would be "just define functions in the traits". The cost of a function call is not what Rust wants to hide, because its systems language nature. This is also why Rust does not have C#-like properties. Scala, on the other hand, adheres to the "uniform access principle", which is very nice, but not suitable for Rust. As they are just plain fields, I don't see why we cannot take their addresses. |
On the topic of a "Scala-like" (very broadly speaking) proposal, I feel the need to cc @netvl. |
Well @CloudiDust, the problem as I see it is that the trait cannot know what an implementation needs. To require a plain value in the implementing type is severely limiting, IMHO. You'd exclude all possible implementations that need to do a calculation to provide a value, for example a lazy eval. |
@arcto I think that bare fields is very suited for "closed inheritance", in that all implementors will be confined to the same crate/module along with the traits, and it is OK to access the fields directly - like in a DOM or AST structure. "Open ended" traits (so to speak), on the other hand, will not contain associated fields, and is intended for implementation by a wide variety of types. (That is, the traits we have today.) The beauty of this solution is, it adds a feature that is actually suitable for a specific use case, but instead of feeling bolted on, it feels very natural. (We have/are going to have associated types/functions/constants on traits, why not fields?) |
Frankly, I just couldn't manage to read and/or understand other proposals in full. For example, all proposals about unifying structs and enums look very complex for little gain. I don't know of any language which does something like that, and this certainly would be a barrier for newcomers to Rust. It is a massive change which increases complexity of the language severely. Proposals which model inheritance via traits, like #223, look much better. They are very lightweight and orthogonal, base on existing set of features and do not complicate the language much. This proposal also looks pretty lightweight, though it is still more complex than trait-based inheritance, but I'm not sure how I feel about some things like field ordering when using associated fields. Other than that it does look like a very nice alternative providing useful features. Associated fields do remind me of Scala, though in Scala there really are no "fields". |
One other way of providing upcasting would be (at the cost of one dereference), to store the parent traits' vtable pointers directly in the vtable of the trait, so the vtable would contain this:
|
### Associated fields Alternatives | ||
|
||
- Deal with abstraction over data as today: Getters, Setter, and the need of virtual calls for trait objects. | ||
- Make mapping not freely choosable, but rather require appropriately named fields to be present in the struct (possibly in the right order as well) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The most common use of associated fields will likely be to delegate them to a TraitData
field defined alongside the trait:
trait Foo {
foo_data: FooData,
}
struct FooData {
x: i32,
y: i32,
}
struct MyFoo {
foo_data: FooData
}
impl Foo for MyFoo {
foo_data => self.foo_data
}
(Alternatively, the FooData fields could be used directly as associated fields and all forwarded individually, but that's even more verbose).
Ideally the default would reduce the boilerplate to specifying the required associated fields in exactly one place, and then declaring that the type wants them in exactly one place. And at that point, there's really no point in explicit mappings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think two use cases of explicit mapping would be "overlapping fields" (two fields in different traits map to the same one in the struct) and avoiding field name clashes (not necessary with fields in other traits, but also the struct's own fields). But the most common case should be more ergonomic.
Admittedly there are more complexity in implementation. I do wonder if these use cases are better solved by other means. But I like that the implementer is completely free to choose local names.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overlapping fields would be better served by having just one trait require the field and having the other trait depend on it (or have them both depend on a third trait with that field). Besides, that's precisely what virtual inheritance does in C++ and the conventional wisdom there is just to fix your design so you don't need it.
Further, while multiple inheritance can be used judiciously, that's quite rare and virtual inheritance even more so. Worse, its implementation details are an absolute nightmare that. Despite being unopposed to MI in general, I am absolutely opposed to anything that complex in the language itself.
Associated fields ought to be very rare and multiple inheritance of them even more so, but if we need to resolve name clashes at all maybe it could be done with a use
in the impl? That would be generalizable to method name clashes too, and potentially even let us reuse existing methods directly in impls.
|
||
This makes it somewhat harder to find out where a given associated items impl comes from | ||
|
||
### Overridable default items Alternatives |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need overrideable defaults? We do currently have default methods in traits, but introducing multiple arbitrarily-deep overrides seems like needless complexity. It shouldn't be very common, and if it's really needed it could be done explicitly with a generic function called from all the leaf types that want the default behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rpjohnst The last line can also be said for the currently supported unoveridable defaults. I think wise use of overriding can be a more intuitive choice. But of course, there will be more implementation complexity in the compiler, so we should see if it is worthy. Also, will this feature somehow help easing C++ interop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The complexity it adds is not worth anything IMO. My suggested workaround generates exactly the same code (modulo one function inline) and makes everything more explicit without adding any verbosity.
As for C++ interop, this shouldn't make any difference- calling C++ from Rust or vice versa already has all the functions and vtables defined, whether with multiple layers of overrides or the workaround I suggested, so the calling side wouldn't be implementing any defaults, let alone overriding them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that it ends up being common when implementing certain patterns.
See libsyntax's fold.rs and visit.rs.
I don't like using a keyword for this myself, especially for the initial implementation - attributes have less dire long-term consequences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, overridable methods is pretty much a design requirement for the servon DOM use case.
This feels pretty over-complicated, with a lot of extra attributes and unnecessary details that might limit the language in the future. I like the idea of associated fields, if they could be made less verbose and complex. I would prefer to do away with field offsets in the vtable- that feels too redundant to me. It may be slightly faster than virtual accessors, but if you really need that performance and multiple inheritance at the same time you may as well do it yourself with It would also be nicer to get rid of Field composition sugar would make the most sense if associated fields didn't require explicit mappings, although it could be nice elsewhere regardless. |
@rpjohnst I think while implementation-wise this proposal may be fairly complex, conceptual-wise it is not. All components are natural extensions to the language and don't feel bolted on (where all other proposals failed, even proposals like #223 give a "for flexible inheritance only" impression, as it is "low level" and its components don't feel like immediately usable day-to-day, though it is actually more flexible). Of course there can be needlessly complex bits and rough edges in this RFC, and those needed to be addressed. And yes, a |
Part of the problem with implementation complexity (especially of the generated code) is that it is more conceptually complex. For example, really understanding all the details and gotchas and corner cases of C++ multiple inheritance requires understanding where vptrs are located, where parent class members are stored, how casts work with offsets, etc. Not to mention this does have significant conceptual complexity with so many moving pieces. Really all Rust needs for inheritance is a way to specify that common fields are at a fixed offset in a structure (
|
I have written and submitted a pull request to address inflexibility with the field composition syntax. The proposed syntax is more extensible, and several potential opportunities to grow the feature are illustrated. (rendered) An example of this syntax would be, given:
This would be equivalent to:
But with the additional property that where |
The inline vtable field is not supposed to be always leading in my original design, the layout would be fixed and then it can take any position. |
@rpjohnst, fair points. :) Indeed if we can implement Still personally I am quite fond of overridable defaults, but it can wait if possible, and we should focus on associated fields in this RFC. |
@eddyb, so basically the "overridable defaults" requirement cannot be dropped? Not that I do not want that feature, of course. |
One interesting point about this proposal is that, like #223, this is very modular, and parts of this can be easily mixed with parts of #223. After reading this, I realized that the rearranging of vtables is also necessary for my proposal, but wasn't explicitly stated. Also, the overridden defaults and ability to call default impls would be incredibly useful in my proposal. Similarly, I this proposal could easily accept things like @glaebhoerl's |
This or #223 is absolutely the way to go. Great work! My only word of caution is that Haskell has grown a number of deriving/default-impl extensions where even together not all problems are solved, and yet there is a ton of redundancy. I worry the default impl / override impl parts of this proposal could open the floodgates and get us in the same experience. My experience with Haskell led me to believe the one true solution is to relax coherence and decouple choosing a canonical impl from writing a impl at all. That way, I believe, all deriving/default patterns can simply be encoded with a bunch of overlapping "polymorphic instances" (aka That however is a large proposal in itself, so if there is interest in it, I recommend leaving the override/default impl portions of this RFC out, and tackling them separately. Also, if we add explicit Vtables, it would be really cool replace trait objects with general existentials: Type ObjDirect<V: Trait> = exists<T>(Vtable<T, V>, T);
Type ObjIndirect<V: Trait, Ptr: type -> type> = exists<T>(Vtable<T, V>, Ptr<T>); People, including myself, have mentioned such things before, but explicit Vtables bring us one step closer. |
@Ericson2314, yes, one of the reasons that I like the "overridable defaults" part is, I foresee that we eventually have to deal with it or variation of it anyway, so it is better to get it right once and for all, and not having a feature that implements "some but not enough" thus create redundancy when we later intoduce other features to deal with the "not enough" problem.. I didn't think of the possibility that If so, we should indeed left out this part from the proposal for now, if possible. Also, full-on existentials is a nice thing to have. But I thought this should be a backwards compatible extension to DSTs which doesn't create redundancy, like HKTs to the generic systems we now have. Or no? If not, then we should also consider this now. |
I remember @glaebhoerl also has some thoughts on full-on existentials, so I think I should cc him again. |
@Ericson2314, still both full-on existentials and enhanced impls may themselves lead to differing coding styles, I suspect, so yes we should be generally cautious when adding those kind of features. I don't think we should do any of these features including inheritance before 1.0, but if full-on existentials is deemed "maybe desirable", then we should make sure the design of DST is compatiable with that feature. (Basically DSTs will be syntax sugars for some common use cases of that.) Hope I am not derailing. |
@Kimundi Another question: how does privacy interact with the field composition sugar? All of your examples used private fields: struct A {
a: uint,
b: uint,
}
struct C {
x: uint,
..A,
c: uint,
} Can you control the privacy on composition? Are all the fields of Is this an intentional design? It seems to "leak" private fields. |
Right, I kinda unintentionally glossed over privacy there. Due to field composition sugar being entirely structural, almost like a macro, you'd probably handle visibility on a more basic level: Enforce that all fields of the used struct are public, or enforce that the struct shares the same visibility space the the place where it is embedd into (Eg, same or parent module). This would solve the problem of "leaking" fields. (Though its really just sugar for redeclaring a field) Associated items of traits currenly always share the the visibility of the trait itself, so
would define Embedding into a struct is a bit more tricky, as there fields can have actual visibility modifiers. I guess the only sane thing here would be to make embedded fields public/private in batch:
vs
|
@aturon, @Kimundi I proposed an alternative syntax that would provide greater control, please see the changes rendered here. I do not know why my alternative syntax is not being considered, but I believe it permits much greater flexibility and clarity. I took the time to write up a pull request and a modified RFC as I was told was the proper way to participate in the discussion and I am now ignored, which frankly does not feel very welcoming. That said, I propose a syntax change to allow greater flexibility in the future. This syntax change to the
I propose:
Which would cause The greater flexibility and potential extensions illustrated by my draft RFC are not part of it, per se. But I believe that it follows norms that Rust appears to follow more closely, w.r.t. modules and hierarchical paths. That is, the |
My apologies, I'm still working my way through the comment thread here and elsewhere, and am focusing on trying to understand the original RFC first before looking at further variations. I'll try to comment on your proposal soon. |
Two questions about
|
Another question about trait Foo {
fn foo(&self);
}
trait FooAlt {
fn foo(&self);
}
trait Bar: Foo {
override fn foo(&self) { }
}
impl Bar for u8 {}
impl Foo for u8 {}
impl FooAlt for u8 {} and what about this variant: trait Foo {
fn foo(&self);
}
trait FooAlt {
fn foo(&self);
}
trait Bar: Foo {
override fn foo(&self) { }
}
trait BarAlt: FooAlt {
override fn foo(&self) { }
}
impl Bar for u8 {}
impl BarAlt for u8 {}
impl Foo for u8 {}
impl FooAlt for u8 {} |
The associated field sub-RFC seems to require explicit mappings struct TextNode {
vtable: Vtable<TextNode, Node>,
parent: Rc<Node>,
first_child: Rc<Node>,
...
}
impl Node for TextNode {} without giving the field implementations for the |
(Or some other combination, like
|
I presume that associated fields, like other associated items, are usable within inherent That would mean you could do the following, inspired by @AaronFriel's PR: struct A {
a: uint,
b: uint,
}
struct C {
x: uint,
inner: A,
c: uint,
}
impl C {
a: uint => self.inner.a,
b: uint => self.inner.b,
} |
@AaronFriel One advantage of your proposal is that you can get a reference to the inner struct A {
a: uint,
b: uint
}
struct C {
x: uint,
parent: A,
use parent {..},
c: uint
} you can say FWIW, I think the |
@aturon: I like the idea of putting an associated fields/use declarations in the
I think that makes it clear that you want to bring names in as aliases. But I would really prefer that if that happened, that it be clear that only fields (and not methods) from the
Alternative syntaxes I could imagine, for various reasons:
This would allow you to do things like:
Should module |
@AaronFriel: Sorry, didn't mean to ignore you, I was just kinda busy and didn't have the time to look at your proposal in detail yet :( @aturon: Yes, you could define associated fields that dispatch deeper into a regular field rather than using composition sugar. I haven't thought much about it yet, but this might be indeed be a way to remove field composition sugar from the picture, and would avoid all the awkwardness about privacy and not being able to refer to the content of the field as its own type. |
@CloudiDust So I was only proposing quantifying over the universe of types, not quantifying over any type. One couldn't write /derail Part of me feels a bit funny about all the associated field stuff, in that it basically boils down to a fancy system to ensure methods like
Everything else proposed would just be macros/sugar -- all the desired semantics / guaranteeing of optomizability would come from the effect system itself. |
@Ericson2314, if I am understanding currently,DSTs are special cases of existentals: Implentation-wise, each existential quantifier would either be in the fat pointer or in the fat object, and we can mix and match. ( But full on existentals may be too powerful so we can end up having multiple ways to encode the same type, which is undesirable. (But may also not be a problem in practice, as the underlying implementation choices are different, as long as we don't allow too much freedom.) /derail An effect system ... huh, another new perspective. Well I am sort of starting to realize why language design is so interesting. :) But I do think that will be overkill this time. |
@Ericson2314, actually if there is a need to expose the |
I misused the name quantifier above. And existential types is indeed a complex feature and not going to happen soon if ever. We'd need variadic generics, a new keyword and tons of magic to make it work. (Or actually we'll "reify" the magic that the compiler performs for DSTs.) Not worth it at least for now. /derail |
|
||
This has the effect that every supertrait is embedded in the vtable of a given trait, at the cost of slight bloat and duplication for every case of multiple inheritance in a trait hierarchy. For single inheritance-only hierarchies, this algorithm will generate the same vtable elements as today, just reordered in prefix order, and thus will have zero additional bloat. | ||
|
||
Once this is done, the compiler needs to allow casts or coercions like `&Trait as &SuperTrait` and implement it by adding a constant pointer offset to the vtable pointer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we already allowed such casts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Huh, seems I am wrong about this - I thought we could explicit cast but implicit cast, but we can do neither.
For this to be viable in Servo's DOM use case, we would probably have to avoid generating duplicate copies of parent class methods for each child class, which is what would happen with naive monomorphization and vtable creation. From a discussion on IRC it seemed like this would be possible, at least with some changes to how reflection works. |
I have seen a few suggestions to make fixed layout a requirement for associated fields. That is not really an option for the more ("idiomatic") use case of being generic over a type implementing a trait exposing some fields. |
After a discussion with @eddyb on IRC, I wanted to clarify some of the problems around this feature. Suppose that trait
In general, it seems better to somehow signal in the impl for Here's another possibility: impl Foo for T { ... }
and impl SubFoo for T { ... } That is, we could provide a syntax for tying (This is just a sketch; more work would be needed to make sure this is a viable approach.) |
Reading the meeting minutes, I just want to leave a quick comment about the "initial call is virtual, all further are statically dispatched" part: Thats the property if you implement methods on a trait as direct trait methods, which makes them virtual in a trait objects:
However, with DST its also possible to directly implement methods on the trait object itself, which leads to statically dispatched code:
So, you get the explicit choice for each method. |
Note that, given that inherent methods trump trait methods, and that these DST-style impl blocks count as inherent methods, you can almost use this pattern to "override" supertrait methods with statically dispatched methods on a subtrait. It works fine when calling methods on the objects of the subtrait type. The problem is that if you ever upcast to the supertrait, you'll end up calling the supertrait version of the method instead. And of course all of this is separate from the explicit default overriding provided in this RFC. You could use them in tandem, but that is not DRY and is error-prone. |
First, thanks for the work on this RFC! We discussed this RFC earlier this week. Some general feedback:
While we're quite sympathetic to the goal of addressing this design challenge via orthogonal extensions to existing features, that must be balanced against the simplicity and ergonomics for the resulting design, and we don't feel like this proposal reaches that balance. In any case, you can read about the plan going forward here; we intend to discuss these designs further in a couple of months. Thanks again for your effort! Closing as postponed. |
Yet another servo DOM proposal, by me and @eddyb
This Proposal is about solving the servo DOM design requirements through a combination of a few orthogonal features and extensions that are useful on their own:
By combining them in the right way, its possible to model something that behaves similar to an inline-vtable single-inheritance OOP system with trait objects.
Rendered view