Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upEfficient single inheritance #142
Conversation
This comment has been minimized.
This comment has been minimized.
nielsle
commented on 6c4f737
Jun 26, 2014
|
I like the idea of adding a field each variant of an enum, but I would prefer to let the variants inherit from a struct.
|
This comment has been minimized.
This comment has been minimized.
|
According to the proposal, the fact that the base item it a struct means that we treat the struct and any sub-items as unsized types. That is, you can only refer to the non-leaf items via a pointer. Thus An alternative to make this work would be to allow enums to inherit from structs and that the first enum acts like a leaf struct in the current proposal,having a fixed size. So |
This comment has been minimized.
This comment has been minimized.
|
I think this a relatively conservative but coherent way of adding single inheritance. /cc @steveklabnik |
This comment has been minimized.
This comment has been minimized.
pepp-cz
commented
Jun 26, 2014
|
Please, no virtual structs, they are so no-rustic! Let me explain ... While reading the swift tutorial, I realized very important difference between rust and swift. In swift struct and class behaves differently. Structs are passed by-value and uses static dispatch for its methods. Classes are constructed on heap, passed by-ref and uses dynamic dispatch. The behaviour is chosen by the creator of the definition and user has no way to change it. On the other hand, rust moves this decisions to user. He can create struct on stack, heap, use Rc, Arc, Gc or anything else to manage the lifetime of the object. He can use type parameters for static dispatch, trait objects for dynamic dispatch. Structs, instantiation and dispatch are perfectly orthogonal to each other. But virtual structs are not. EDIT: I have found out that similar approach was suggested in #9 so the rest of the comment is obsolete. I have got an idea how to deal with this but I have not produced RFC yet because I do not know how DST should work. The idea is that virtual structs are basically useful only for DOM-like data structures (generally some polymorphic graph data structures). So we provide object wrapper and type-erasing reference type in std that can be used to build such structures. Then, you could add any type implementing particular trait to the structure, even primitive types such as int or float. Decision is in the hands of the user. Very schematically:
Then, you can wrap instances into PolyObject that can build PolyRef to itself, store PolyRefs into the data structure. Use PolyRefs to build trait object when you need to work with the objects. The tricky part is how to make this play nicely with smart pointers. Here I lack the knowledge about DST which can be probably used for that. For example, if PolyObject is type-erased DST wrapper of an object, It can be boxed into smart pointer. And PolyRef would not be needed. However I do not know if this can be done. The code could look like this:
With some typedefs and Unref impl it can be made more readable. |
This comment has been minimized.
This comment has been minimized.
|
Type erasure is what you don't want here. What you described is exactly On Thu, Jun 26, 2014 at 4:12 AM, pepp-cz notifications@github.com wrote:
|
This comment has been minimized.
This comment has been minimized.
pepp-cz
commented
Jun 26, 2014
|
Trait objects carry the underlaying object by value? If so, then they are what I have described. But the core message of my comment was different: Do not introduce virtual methods on structs, use trait objects instead. If I read it correctly, (virtual) structs do have vtable pointer attached to them. That is exactly what I meant to advise agains. If you use such struct in a monomorphic data structure, than the pointer is of no use. Here the creator of the struct decided and user cannot change the decision. If the struct was just regular struct, user can use it directly in the monomorhic structure or via trait object in a polymorhic one. You can build such structure from unrelated types of objects, no inheritance required. It should be more flexible. Inheritance will be just a tool for code reuse. |
lifthrasiir
reviewed
Jun 26, 2014
|
|
||
| Open question: should we use `()` or `{}` when instantiating items with a mix of | ||
| named and unnamed fields? Or allow either? Or forbid items having both kinds of | ||
| fields. |
This comment has been minimized.
This comment has been minimized.
lifthrasiir
Jun 26, 2014
Contributor
I think we should constrain ourselves to {} when it comes to sort of named arguments. In general I was thinking something like this:
let x: E2 = E2 { f: 34, Variant2(23) };That is, the current struct literal would be extended to a general data type literal. A list in the braces would then contain zero or more fields (<id>: <expr>), zero or one variant (<path> or <path>(...) or <path> { ... }), and zero or one struct extension (..<expr>) in this exact order. The existing simple variant, say, Some(42) would denote a shorthand for Option { Some(42) }. This syntax is consistent to how the syntax extension syntax would work with such struct-like enums (let y = E2 { Variant, ..x }; let z = E2 { f: 42, ..x };).
This comment has been minimized.
This comment has been minimized.
|
IMHO, making enums and structs "unified but subtly different" is very confusing and not orthogonal at all. Also "enums with fields" are no longer enums, if we ever go down this road (which I am against), "classes" would be a somewhat better name (and we don't actually want a second C++, do we?) Inheritance should only be used in a handful of scenarios as a last resort, and I don't suggest making such changes to the language only for it. How much can we strip out of this to get down to the bare minimum of changes necessary? |
This comment has been minimized.
This comment has been minimized.
dobkeratops
commented
Jun 26, 2014
|
copying c++ single inheritance doesn't seem bad to me; rust fixes the serious problems of unsafety and headers. increasing the subset of existing C++ code you can interact with directly would be a good thing, IMO, for getting greater adoption. I'm surprised there wasn't more interest in the idea of generalizing how trait objects work though... leverage the existing way of representing vtables- provide some safe sugar for the internal vtable hack that is currently possible in rust. |
cmr
reviewed
Jun 26, 2014
| Example (in pseudo-code): | ||
|
|
||
| ``` | ||
| class Element { |
This comment has been minimized.
This comment has been minimized.
cmr
Jun 26, 2014
Member
This example is ancient, confusing, and potentially misleading. It should just be removed, or updated to look like a real language in existence.
This comment has been minimized.
This comment has been minimized.
|
@dobkeratops : The problem is, if Rust provides inheritance in the core language, people will use and inevitably overuse it for things where better solutions are available. (And for most use cases, inheritance is not the best answer.) Maybe keeping this behind a feature gate (even after it becomes stable) can make things better?
But then, inheritance as proposed in this RFC seems to require extending and unifying enums and structs, which would then differ mainly in how they would be passed, and how much space they would take. That doesn't seem very orthogonal to me. Yes, they will be "typically" used for different cases, but there would be no guarantee. Rust used to have a condition system, but it was later removed to simplify the language. One of the reasons for the change, I believe, was that conditions were not quite orthogonal to |
This comment has been minimized.
This comment has been minimized.
|
The condition system wasn't part of the language. It was a macro plus some On Thu, Jun 26, 2014 at 7:24 AM, Ruochen Zhang notifications@github.com
|
This comment has been minimized.
This comment has been minimized.
pepp-cz
commented
Jun 26, 2014
|
@dobkeratops |
This comment has been minimized.
This comment has been minimized.
ben0x539
commented
Jun 26, 2014
|
This looks much more complicated than what I had in mind when people talked about unifying structs and enums. Also I'm surprised there's vtables involved but it's a completely distinct system from traits. |
bstrie
reviewed
Jun 26, 2014
| Drop is marked `inherit`. | ||
|
|
||
| It is the programmer's responsibility to call `drop()` for outer-items from the | ||
| impl for the inner item, if necessary. |
This comment has been minimized.
This comment has been minimized.
bstrie
reviewed
Jun 26, 2014
| and may in turn be overridden. A method without the `virtual` annotation is | ||
| final (in the Java sense) and may not be overridden. | ||
|
|
||
| Open question: alternative to `virtual` keyword - `dynamic`. |
This comment has been minimized.
This comment has been minimized.
bstrie
Jun 26, 2014
Contributor
I tend to prefer dynamic to virtual. It also seems like it could potentially be reused more.
This comment has been minimized.
This comment has been minimized.
bill-myers
commented
Jun 26, 2014
|
Using "struct" and "enum" to denote being sized or not seems far less natural than using "struct" to denote instantiable leaf nodes, and "enum" to denote non-instantiable non-leaf nodes, and using the "unsized" keyword to denote unsizedness if needed. Note also that you can represent enum variants using their minimal size just fine, as long as copies from a dereferenced enum pointer first determine the actual variant size instead of doing a blind memcpy of the whole enum size. The real semantic issue of "unsized" vs "sized" happens when you can't represent something as constant-sized because the size of variants is unbounded: this is the case if you allow to introduce generic parameters in variants (and then declare a field of that type), and if you allow to inherit outside the crate. Also, having a vtable pointer or an enum discriminant is semantically equivalent (you can match on the vtable pointer value, and you can lookup virtual functions in an array with an enum discriminant), so I don't think this should be a fundamental part of the semantics. Making non-leaf structs both overridable and instantiable seems problematic, especially if you make them unsized, since it means that obvious code like Overall, I think the struct/enum design in #11 is better, although I think the method dispatch in #11 vs the traditional Java-like method dispatch (with the ability to override implemented methods in addition to abstract ones) proposed here and elsewhere is more a matter of taste. |
bstrie
reviewed
Jun 26, 2014
| Do we need multiple inheritance? We _could_ add it, but there are lots of design | ||
| and implementation issues. The use case for multiple inheritance (from bz) is | ||
| that some DOM nodes require mixin-style use of classes which currently use | ||
| multiple inheritance, e.g., nsIConstraintValidation. |
This comment has been minimized.
This comment has been minimized.
bstrie
Jun 26, 2014
Contributor
And if we decide not to implement anything akin to multiple inheritance, are we screwed? I sure hope the DOM isn't that tightly coupled to C++-specific design decisions.
This comment has been minimized.
This comment has been minimized.
|
I would like to see @pcwalton weigh in on how this proposal would satisfy Servo's requirements. Furthermore, it would be very premature to accept any proposal of this magnitude until @nikomatsakis gets back from vacation and has a chance to consider it. |
This comment has been minimized.
This comment has been minimized.
|
One thing that I don't see mentioned concretely: is this change entirely backwards-compatible? I'm not asking from a wait-till-after-1.0 standpoint (though keeping it gated till afterwards would be nice, if possible), I'm asking because I want to know whether or not our current tutorial material regarding structs and enums will continue to be relevant. It would be nice if the current "division" between structs and enums would suffice to teach people the semantic differences (as well as introduce them to tagged unions with a minimum of noise), and only once they grok these two concepts do they need to be introduced to this advanced material. |
This comment has been minimized.
This comment has been minimized.
|
One more thing: I'm concerned that this is a lot of complexity in order to support such an allegedly narrow use case. In my gut I feel like there's more simplification that could be done here, though unhelpfully I don't have any suggestions. |
This comment has been minimized.
This comment has been minimized.
|
Structs and enums don't change very much from their current meaning, they are just extended to newer meanings. I agree that this seems to be a lot of complexity though. |
This comment has been minimized.
This comment has been minimized.
rpjohnst
commented
Jun 26, 2014
This comment has been minimized.
This comment has been minimized.
|
The only use case repeatedly brought up is the DOM, which I don't think is a good example motivating situation. The DOM is a not a fundamental problem, it is an existing solution designed around other languages' strengths and weaknesses. The goal for rust should be to have some solution the problems it tackles, not to support every other languages method's of solving those problems. Trait objects already let one express heterogeneous data structures like the DOM, the problem is just performance. I think a series of optimizations and additional representation choices would be enough to get what Servo needs out of trait objects, for example:
Maybe macros or a compiler plugin could be used to fake inheritance on top of this, but I'd worry that putting it in the language itself will add a lot of complexity to the language, and be something new users flock to out of familiarity when there are better ways to get the job done. |
This comment has been minimized.
This comment has been minimized.
As far as Mozilla is concerned, the primary goal of Rust is to be a platform for Servo. Servo cares very much about making the DOM as efficient as it can. This leads to the conclusion that the DOM is in fact the only motivating situation that truly matters for this case. |
This comment has been minimized.
This comment has been minimized.
|
@kballard, I don't disagree. It's very important to cater to issues that Servo has encountered. However, by failing to look for other use cases, we may inadvertently be designing ourselves into a corner. It would be valuable to consider other domains where this feature may be desirable, and ensure that our design supports those use cases cleanly (or not, if we'd prefer that they use some other Rust mechanism instead). That said, if this feature is literally useful only because of the need for Servo to support the DOM, and no other Rust code ever written could conceivably ever ever want to use this, then I'd prefer a hacky gated solution to something far-reaching and complex. |
This comment has been minimized.
This comment has been minimized.
|
A few thoughts:
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Now that I think about it some more, the subtyping question can probably be set aside for the moment, since extending this proposal with subtyping is backwards compatible. |
This comment has been minimized.
This comment has been minimized.
engstad
commented
Jun 29, 2014
|
@pcwalton It would only require boxed allocation. As a matter of fact, you can already do this manually: enum Axis { X, Y, Z }
struct Inner {
left : Box<Node>, // sizeof(Node) > sizeof(Box<Node>)
rite : Box<Node>,
pos : f32,
axis : Axis
}
struct Tri {
indexes : [u16, ..3],
flag : u16
}
enum Node {
Inner(Box<Inner>),
Leaf(Box<Tri>)
}My point is that it would be nice to have a way of doing this without having to define named types for each type appearing in the enumeration data. The compiler AST could be reduced in size in a similar fashion, but as you can see from the work involved, it is easier and nicer to not do this. @pczarn Yes, that would be unfortunate. The tagged DST approach sounds very interesting, by the way. |
This comment has been minimized.
This comment has been minimized.
nielsle
commented
Jun 30, 2014
|
How about allowing an enum to use an existing (Sized?) struct definition as a variant? (Edited) struct Sprite{ x:int, y: int, gif: ... stuff goes here.. }
#[unsized]
enum Shape {
struct SpriteVar = Sprite,
Circle(int,int,int),
Rectangle(int,int,int,int)
}
let x: SpriteVar = Sprite {x: 2, y:2, gif: ... stuff goes here.. }
match x {
s @ SpriteVar => { .... }
Circle(x,y,r) => { .... }
Rectangle(x1,y1,x2,y2) => { .... }
}That allows you to use the same syntax for struct objects and enum variants, so you can change back and forth without having to rewrite all your code. The match statements should allow you to match for struct-variants that implement a certain trait. trait Drawable { ... };
impl Drawable for Sprite { ... };
match x {
s @ Drawable => { .... }
....
}Matching for
|
This comment has been minimized.
This comment has been minimized.
bill-myers
commented
Jun 30, 2014
|
@nikomatsakis Does the distinction between sized/unsized enum/struct really need to made when declaring the type? Isn't it possible to make enums behave both as unsized and as sized depending on context without making too many compromises? (when declaring a field of enum type it would be sized, but when declaring an &Enum it would be unsized) Or alternatively, adding syntax to make any enum unsized? E.g. "None" would be a zero-sized data type, but "Option[None]" would be an Option data type constrained to be None, and "&mut Option[*]" would be a DST pointer, while "&mut Option" is a pointer to Option. |
This comment has been minimized.
This comment has been minimized.
sinistersnare
commented
Jun 30, 2014
|
As someone who has not been really following this discussion, can someone explain what the difference between It seems as if |
This comment has been minimized.
This comment has been minimized.
|
@nikomatsakis @bill-myers Yeah unsized layout and open datatypes are orthogonal, so I'd be wary to have the former motivate the latter. While ASTs are brought up I like "Two-level types" approach as described in http://blog.ezyang.com/2013/05/the-ast-typing-problem/ , to be able to add more variants or more fields in common to all variants. I'd guess unsized + the type that ties the knot not boxing the core AST + packing the discriminants together, would be all that's needed to make this just as performant. #![unsized_fat_enum, packed_discriminants]
enum LValue<LV, RV> { ... }
enum Statement<LV, RV> {
Mutate(Box<LV>, Box<RV>),
...
}
enum RValue<LV, RV> {
Block([Box<Statement<LV, RV>>])
LValue(LV),
App(Box<RV>, [Box<RV>]),
...
}
mod Typed {
struct Typed<T> (Type, T)
struct LValue(Typed<super::LValue<LValue, RValue>>)
struct RValue(Typed<super::RValue<LValue, RValue>>)
}
mod Plugin {
trait CustomLValue { ... }
trait CustomRValue { ... }
enum Custom<S, C> { Std(S), Custom(C) }
struct LValue(Custom<super::LValue<LValue, RValue>, CustomLValue>)
struct RValue(Custom<super::RValue<LValue, RValue>, CustomRValue>)
} |
This comment has been minimized.
This comment has been minimized.
|
On Mon, Jun 30, 2014 at 06:38:29AM -0700, bill-myers wrote:
It is perhaps possible but presents numerous complications. For one
we'd have to generate some complex code that checks whether this For another, we need to distinguish lvalues that will be mutated from
Here, you might think that we could allocate a very small box for
(It is true that the contents of an |
larsbergstrom
referenced this pull request
Jul 16, 2014
Closed
Tracking issue for Rust feature requests #2854
jdm
reviewed
Aug 8, 2014
| They have a tag and are the size of the largest variant plus the tag. A pointer | ||
| or reference to an enum object is a thin pointer to a regular enum | ||
| object. Nested variants should use a single tag and the 'largest variant' must | ||
| take into account nesting. Event if we know the static type restricts us to a |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
@nrc I was talking with @jdm on IRC and we think we have a way to initialize private fields of superstructs without exposing them (and also to modularize constructors, to some extent). The basic idea is to leverage FRU by allowing one to write:
Essentially, it is permitted for you to write |
This comment has been minimized.
This comment has been minimized.
|
@nick29581 in that case, we could remove the module requirement and just limit subtypes to the same compilation unit. @nick29581 also, I've been thinking that this proposal is really not about making a specialized variation of trait objects, nor object-oriented programming per se, as it is about making a generalized version of enums to cover more use cases (like refinement and being able to allocate instances of exact size that cannot change variants). I think we should change our nomenclature somewhat appropriately, though I'm not 100% sure what to change it to. |
This comment has been minimized.
This comment has been minimized.
|
@nick29581 just remembered that super types are unsized, so returning them doesn't work. Still, if we could find some scheme like that, it would be good. |
This comment has been minimized.
This comment has been minimized.
jdm
commented
Aug 8, 2014
|
My only remaining concern after the hierarchy/inherited field initialization stuff is addressed is calling inherited methods without requiring explicit casting. @nikomatsakis tells me that that's going to be discussed, so I'm pretty happy with this proposal. |
This comment has been minimized.
This comment has been minimized.
|
This RFC proposes that all variants of an enum/struct are usable as types, and the names don't form a hierarchy, but are "flattened" and poured into the enclosing scope. And when doing a match, we can use any names from any level of nesting to cover all inner levels. This may be a breaking change. Currently it is possible to write code like this: enum Foo {
Foo,
Bar
}
match Foo {
Foo => println!("This is the Foo variant!"),
_ => println!("This is the Bar variant!")
}That's because currently we cannot match against the enum name itself, and the compiler knows that But if this RFC is implemented, the two So may I suggest that we forbid enums having namesake variants, now? However I'd like there to be an exception, that is when this namesake variant is the only variant of the enum: enum Foo { Foo(Bar) }In this case, only a single type That's much like Haskell's data Foo = Foo BarAnd to deal with the weirdness that "a Rust enum can have fields", I think we can forbid fields at the "top level" of the enum, and instead require a "wrapper variant". So, enum Foo {
bar: Bar,
Variant1 {
baz: Baz,
qux: Qux,
Variant2
},
Variant3(int, int)
}becomes: enum Foo {
Foo {
bar: Bar,
Variant1 {
baz: Baz,
qux: Qux,
Variant2
},
Variant3(int, int)
}
}More typing, but no one would think that Foo is not an enum. Besides, are variants required to come after the fields? (I think so, but this fact is not clear in the RFC.) |
pnkfelix
reviewed
Aug 21, 2014
| Variant1, | ||
| Variant2(int), | ||
| VariantNest { | ||
| Variant4, |
This comment has been minimized.
This comment has been minimized.
pnkfelix
Aug 21, 2014
Member
when I first saw this nested enum syntax, I was worried that it conflicted with struct-style enum variants; i.e., how does one distinguish the case shown here from enum Names { Variant1, Variant2(int), VariantNest { x: int } }
But on reflection, I think part of the reason you have picked the syntax shown here and above is precisely that it supports (unifies) the struct-variant syntax with your named-field syntax shown above.
You allude to this in the presentation above when you say that this RFC makes struct-variants and tuple-structs less ad-hoc.
I think you should go further and actually add some concrete examples showing how the struct-variant syntax looks and how it is subtly distinguished from (or, if you prefer, "unified with") the nested enum syntax.
nrc
self-assigned this
Sep 4, 2014
alexcrichton
force-pushed the
rust-lang:master
branch
from
6357402
to
e0acdf4
Sep 11, 2014
aturon
force-pushed the
rust-lang:master
branch
from
4c0bebf
to
b1d1bfd
Sep 16, 2014
nrc
added a commit
to nrc/rfcs
that referenced
this pull request
Sep 17, 2014
This comment has been minimized.
This comment has been minimized.
|
This may have been mentioned somewhere but I didn't see it. Instead of virtual functions, how about just using traits. Basically, use traits to describe functionality that must be implemented by children. First, add the ability to specify that a struct(/enum) must implement a trait (this might also be useful for compile-time optimizations): struct X: Y; // There is a struct X that must implement trait Y
impl Y for X {
// ... A required implementation
}This is consistent with the current trait syntax because the Given this feature, one could write the following:
Due to the The primary reason I prefer this over virtual functions is that it is consistent with the current use of traits. The secondary reason is that it doesn't allow for method override chains (which, in my experience, tends to lead to unexpected behavior). If you need to "inject" some functionality in the middle of an inheritance chain, you can just create another trait:
|
This comment has been minimized.
This comment has been minimized.
|
@Stebalien, there is a variation of this proposal that uses trait for method dispatch: #245. And for pure trait based inheritance (which enhances traits, but do not modify There are also #9, #91, #223, which provides special structs/traits to do inheritance, but do not directly enhance normal structs/enums/traits. |
This comment has been minimized.
This comment has been minimized.
|
Well #250 does provide field mapping and some attributes which can be seen as enhancements to structs. |
This comment has been minimized.
This comment has been minimized.
|
Closing in favour of other RFCs which address the same problem (see http://discuss.rust-lang.org/t/summary-of-efficient-inheritance-rfcs/494), in particular #245. |
nrc commentedJun 26, 2014
Efficient single inheritance via virtual structs and unification and nesting of structs and enums.