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

Trait based inheritance #223

Closed
wants to merge 6 commits into from

Conversation

gereeter
Copy link

@gereeter gereeter commented Sep 1, 2014

This is largely based upon #9 and #91, but fleshed out to make an actual inheritance proposal.

Moreover, in comparison to other proposals for inheritance, the design should work
well with existing Rust features and follow Rust's philosophies:

* There should be no new ways to acheive the same behavior. For example, virtual calls
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/acheive/achieve/

@rpjohnst
Copy link

rpjohnst commented Sep 5, 2014

👍 This is how Rust inheritance should work. This level of flexibility could even allow for bindings with other object models- various C++ layouts (potentially even with multiple inheritance), COM, etc.

@zwarich
Copy link

zwarich commented Sep 5, 2014

How will safe downcasting work with lifetime parameters? In the example, Node has no lifetime parameter but TextNode and ElementNode do. As described, it seems that downcasting with transmute will just introduce a new lifetime parameter out of thin air, which isn't correct.

For downcasting to be safe it seems that all members of a class hierarchy need to have identical lifetime parameters, the copy of a parent class in a child class needs to be instantiated with the same parameters as the child, and the downcasting operation needs to respect these parameters.

@gereeter
Copy link
Author

gereeter commented Sep 6, 2014

I believe the issue with these lifetimes that seem to pop out of thin air was fixed by #192. Basically, because TextNode<'a> only implements Extend<NodeData>, not Cast<NodeData>, there is no way to convert from a TextNode<'a> to NodeData in an owned context. The are only two ways to get NodeData from a TextNode<'a>. First, when stored behind a reference, the two representations can be switched. However, this is perfectly safe, as the text must outlive the reference. Second, the TextNode<'a> could be converted to a trait object of Node. Due to #192, this must also be safe, as the trait object must be annotated with an explicit lifetime bound.

I'll try to update the examples to make this more clear.

@CloudiDust
Copy link
Contributor

+1 to this proposal.

As pointed out in the proposal, the traits here can be the low level building blocks, providing maximum flexibility, while commonly used inheritance patterns can be codified in macros. (Sort of like Ecmascript 6's class syntax vs the underlying prototype-based OO which is both more flexiable and more "alien".)

It will make people think twice before using inheritance, simply because the relative verbosity and the "ugliness" of macro invocations compared to dedicated syntax.

I consider this a plus and don't think we need to artifically limit inheritance in any other way. (IMHO, #142 needs limitations partly because the syntax there is too lightweight and pretty. That can be both an advantage and a disadvantage.)

that something of type `A` can be converted to something of type `B` safely with a
simple `transmute`. To actually use this ability, a function is added to the standard
library as follows:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cast is almost the wrong name for the trait, because casting in both Rust and C is a conversion preserving the value, not something like a blind transmute.

@zwarich
Copy link

zwarich commented Sep 7, 2014

@gereeter That approach doesn't work with multiple lifetime parameters, does it? Also, what about non-lifetime type parameters?

@gereeter
Copy link
Author

gereeter commented Sep 7, 2014

@zwarich The same approach should work fine for multiple lifetime parameters: in the trait object case, the object would have to have all the lifetime bounds, and in the reference case, any reference would have to outlive all lifetimes involved.

For non-lifetime type parameters, the parameter would be part of the RTTI.

}
```

## Summary example
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For easy comparison with the other proposals for handling DOM-data structures, could you show how https://gist.github.com/jdm/9900569 would look encoded with this proposal please?

@brson brson assigned alexcrichton and unassigned nrc Sep 9, 2014
@alexcrichton
Copy link
Member

@gereeter, could you add an example of how https://gist.github.com/jdm/9900569 might be encoded with this RFC? The example at the end looks similar, but not quite the same.



fn dump<'a>(node: NodeBox<'a>) {
if let Ok(text_node): Option<&TextNodeBox<'a>> = downcast_copy(node) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this cleverly crossborrowing node into a shared reference? (Probably should be Some(...), not Ok(...))

@nielsle
Copy link

nielsle commented Sep 13, 2014

Bikeshedding. Could the following syntax work? Here @Coerce should "magically" ensure that the layout is fixed.

@coerce(Node)
struct MyNode { node: Node,  .... }

Perhaps this could open the door for future self inspection syntax..

@coerce([f32,.. 3])
struct MyVec { x1: f32, x2:f32, x3:f32 }

let xs = MyVec {x1:0, x2:0, x3:0}
for x in cast(xs).iter() { .....}


```
impl <U, T> Cast<Bundle<U>> for Bundle<U, T> {}
impl <U: Cast<U2>, T: Extend<T2>> Cast<Bundle<U2, T2>> for Bundle<U, T> {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct? It seems like it should either be T: Cast<T2> rather than Extend or impl Extend rather than Cast. (T: Extend<T2> does not imply that Bundle<U2, T2> and Bundle<U, T> have the same representation, which is what impl Cast is stating.)


## Downcasting (safe RTTI)
The only item left on the list of requirements for inheritance is the problem of downcasting. To deal with this,
yet another trait is introduced, `Typed`. This trait has two properties that make it unique:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is essentially the same thing as the existing Any trait1.

1 Except that as the current Any is implemented, it is known to be impled by all types T: 'static, which is obviously wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

obviously

It's not to me, could you explain in a little more detail?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn evil_id<T: 'static>(mut arg: T) -> T {
    if let Some(an_int) = (&mut arg as &mut Any).downcast_mut() {
        *an_int = 666i;
    }
    arg
}

I shouldn't be able to do this. I should have to specify T: Any. (I should be infer from the absence of an Any bound that the function won't do something like this. 'static should only mean 'static.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'static should mean 'static and impl<T: 'static> Trait for T should also mean that you always get Trait when you have 'static - I see no reason why this is at all unreasonable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with what you wrote, but I was never implying otherwise.

The unreasonable part is having impl<T: 'static> Any for T in particular. We should have compiler-generated impls for each type individually, but not a blanket impl for all types. The Any bound should be satisfiable by any concrete type, but not by abstract type parameters except if one explicitly writes T: Any. (As this RFC also states.)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still unsure why this actually needed.

Wouldn't

impl <T: 'static> Typed for T {
   static TYPE_ID: TypeId::of::<Self>()
}

do the same job as an impl for all types? I'm not clear on why it's important that I have to write specifically T: Typed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that contracts are explicit and type signatures actually mean something. If we allow dynamically casting any abstract type to any other type, then a function requiring T: 'static could do just about literally anything (see above evil_id). It should be possible to know that a generic function only manipulates / makes use of its type parameters in accordance with the trait bounds which it specifies. I don't know how else to rephrase this to make it clear why this is desirable.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean you are proposing we should remove Any?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I just think it should be implemented differently. I would prefer (as, again, this RFC we're commenting also proposes) to remove the blanket impl<T: 'static> Any for T impl (it's actually AnyPrivate but that's unimportant), and instead, whenever you write:

struct Foo;

the compiler automatically derives:

impl Any for Foo { ... }

@nwin
Copy link

nwin commented Sep 21, 2014

How about renaming #[first_field] to #[parent]? The RFC seems to be conceptional close to Self where parent objects are mostly saved in a slot called parent. So it would just seem natural to mark the parent object with #[parent].

This was referenced Sep 21, 2014
@brson
Copy link
Contributor

brson commented Sep 23, 2014

The consensus among the core team is that while this is an orthogonal set of abstractions, in actual use they do not present an ergonomic user experience.

Discussion

@brson brson closed this Sep 23, 2014
wycats pushed a commit to wycats/rust-rfcs that referenced this pull request Mar 5, 2019
Add `isEqual` named export to`@ember/utils`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.