Pre-RFC: Disjoint Polymorphism

Thanks for the notice!

On the advice of @cmr I have been trying my hand at a prototype which revealed some things that did not quite work. Once the prototype is working, I’ll push it to github and link it here, then I’ll review this proposal in light of the lessons learned.

As a heads up, for example, I’ve been planning to remove all that talk about “Coerce” traits; the fact that they are used both as “marker” for inheritance and to actually coerce violates the Single Responsibility Principle which seems like poor design, I’ll be replacing them with “just” inheritance markers. I’ll also have to change the exact layout of VTable/Class, as I had not fully realized how Open Inheritance prevented full knowledge of what traits a struct implemented which means moving some stuff around.

A rough prototype has been push to github: https://github.com/matthieu-m/rust-poly

There are two outstanding issues:

  • [repr(C)] and custom Drop implementations apparently do not play well with each others, I suspect this means that the body of DynClass and Class should be revised
  • T, as a generic parameter, cannot be declared to be a trait; this means writing constraints annoying (I introduced a trait to indicate that a struct was implementing a trait…) but it also means that the nightly version is refusing to transmute TraitObject to &T (even when T is marked ?Sized)

It is unclear whether those problems happen enough in the wild than a full-fledged solution is necessary or whether a local hack (but which) would be sufficient.

Hm, what is the problem here? Off the top of my head I can't think of any issues that would cause, except for potential bugs in the zeroing code.

The issue with this is that even though T could be unsized, it isn't guaranteed to be unsized, so &T could be either one word or two. You can work around this with transmute_copy, but in the case that T is sized, it will be incorrect.

I get the following warning when implementing Drop for a #[repr(C)] type: warn(drop_with_repr_extern) (activated by default), the text of the warning suggests that implementing Drop introduces hidden state into the type.

Thanks for the transmute_copy trick; I’ll just have to trust in the user not to pass anything weird for now, but at least it’ll get the prototype going.

Just wanted to let you know that I have updated my experimental repository with the latest approach: https://github.com/matthieu-m/rust-poly

I’ve got a crash in the drop glue generated by rustc (probably my fault for fiddling with Drop in the first place), but it only occurs at the end of the demo code and we can already see polymorphism in action so I am not too worried.

My next step will be to rewrite the RFC from scratch based on the experience gained with the prototype (@cmr was definitely right that the experiment was necessary). I’ll try and take care to distinguish between language/compiler changes and the library changes. Essentially they seem to boil down to:

Language/Compiler changes:

  • Addition of raw::{StructInfo, TraitInfo, VTable} and intrinsics::{struct_id, trait_id}
  • Addition of intrinsics::{struct_info, trait_info, v_table} (the _by_id versions being part of the kludge)
  • Necessary language changes to support data inheritance, and the various “where” clauses

Library changes:

  • Addition of the *Cast* traits
  • Addition of Class and DynClass (supported by VRef and UntypedVRef)
1 Like

First of all: I apologize for taking so long to reply to you; I first wanted to clarify where I was going before answering your points, rather than hand-wave them, and it took much longer than I had anticipated.

Coercible...

So, regarding all this talk of coercion, I actually plan to remove it altogether.

I used a generic term to describe a very specific relationship (parent-child) and on top of that there was indeed a conflation of uses: both a marker trait, and an implementation.

I will instead recommend using a specific vocabulary as part of the RFC.

#[parent]

There are undeniable advantages to using an attribute on an existing data-member, not the least of which is the easiness to refer to said data-member. Or of looking it up on Google (named entities are so much easier to look up than symbols).

On the other hand, it seems more "tacked-on" (why would traits be special-cased?) and expressing bounds is less obvious (whereas if you use Child: Parent like trait, you expect Child: Parent to be a valid bound syntax).

Ultimately, it is something I'll leave up to debate, and I'll mention the #[parent] syntax in the alternative section of the RFC so that the community may decide.

This feels very nice, though I'm not completely clear on what would happen if multiple parents implemented the trait.

The compiler would simply refuse to automatically implement the offending methods, complaining about the ambiguity, and the user would have to specify them manually.

It would be better if the compiler could perform the up-casts implicitly, much like with Deref, there seems to be little point in having the user fiddling with as over and over.

This seemed really cool, until @cmr mentioned the potential issues in specifying the algorithm of what to deref to... I plan to strike this down and instead propose a lightweight (but explicit) syntax such as child as &Parent. I'll let the author of Child decides whether it's worth it to implement Deref<Target = Parent> or not.

unsafe impl<T, S> Coerce<Box<DynClass<T, S>>> for Box<Class<T, S>> { }

For now I just moved to convert::From. Works pretty well!

fn up_cast<T, S, B, P>(original: Box<DynClass<T, S>>) -> Box<DynClass<B, P>> where T: B, S: P, S: CoerceRef<P>;

Similarly, this could be done via Coerce.

Actually, I moved on to specific [Up|Down]Cast[Ref|]<T> traits, and simply provided an implementation of UpCast<Box<DynClass<B,P>>> for Box<DynClass<T,S>>.

Extend

My mistake, I read Trait: Extend<Struct> in your RFC and mistakenly concluded that it meant the trait had fields, where it was in fact just a bound. I should have been more careful.

My general feeling is that I definitely like this proposal, probably in large part because of its similarity to the proposal I made. I think that focusing on having a set of features that do not step on each others' toes and work very well together is incredibly important, and I think you have done a good job at doing that.

I think so too! On the recommendation of @cmr I made a prototype you can find at GitHub - matthieu-m/rust-poly: Exploration of polymorphism in Rust and I'll now be re-working the RFC based on the experience I gained from it and the feedback I received from here.

I expect it to take me some time, but hopefully by the end of the week I'll have something I am not too ashamed to show off.

Thanks for your detailed feedback, and once again, my apologies for taking so long to answer.

1 Like

Take your time. We really want you to get this right. :smile:

1 Like

@aturon, @nikomatsakis, @cmr

I finally got around to whip the prototype that @cmr launched me on into shape, and I think that the result is not too bad (though its performance is likely bad and it is very much untested).

Based on the prototype, I have also considerably revised the RFC, which I decided to host on github alongside its implementation: https://github.com/matthieu-m/rust-poly/blob/master/doc/0000-disjoint-polymorphism.md

I have a couple tweaks here and there in mind, but only implementation details, and therefore I am very much interested in gathering feedback on this second version of the RFC. It is actually a bit longer than the previous iteration (closing on 44,000 characters), even though several “features” were expunged.

There are, however, some Unresolved Questions:

  • Could DownCastRef and UpCastRef be supplanted by implementing the regular DownCast/UpCast on references instead? Or would that introduce ambiguities when calling .up_cast() on a reference to a type that already implements UpCast?
  • How to provide cloning? Beyond Clone not being object-safe today, it also does not work with a raw memory area, furthermore, in the absence of negative bounds, it seems impossible to implement a function (or set of) returning a type-erased Option<ClonerFn>. Compiler magic would work, obviously.
  • How to provide a safe ?Sized type in the absence of Custom DST? Is it even possible?

And I am seeking a clarification:

  • What does “sharing methods between definitions” mean, exactly? Is this something beyond what traits already give?

Just letting everyone know that I just updated the repository with a couple goodies. Apart from code tidying and performance improvements (moving away from run-time pointer chasing) I managed to find one way to solve the “cloning” unresolved question (a RawClone trait).

EDIT: Removed any talk about Clonable and capabilities; it took me a week to find this solution, and only half an hour after pushing it did I realized that I had overlooked the very simple fact that traits are capabilities, already, that they can be checked at compile-time, already, and that this RFC was introducing a way to check them at run-time…

I haven’t read through the full proposal (there really is a lot to get through), and this isn’t really directly related to the problem the RFC’s trying to solve, but I’d like to say that while I’m really excited by the trait polymorphism section of the RFC, I think that it could potentially be made into a much more general feature—possibly part of HKT. I’ve always viewed trait polymorphism as a form of HKT, adding trait as a new kind (not just a bound) which specifies that a parameter must be a trait (not a trait object type, as the RFC seems to require). That is, saying T: trait would not imply that T could be used as a type, because T isn’t necessarily object-safe. To rectify this, there would have to be a built-in ‘trait trait’/‘metatrait’ (a trait that is implemented by traits instead of types, like ExtendTrait in the RFC) called ObjectSafe that would automatically be implemented by all object-safe traits. So the RFC’s guarantee that

  • if T: trait, then let t: &T = mem::transmate(raw::TraitObject { ... }) is a valid (and unsafe) expression

would only be valid if T was also bounded by ObjectSafe (T: trait + ObjectSafe).

I believe ConstraintKinds in GHC allows something similar, but I’m not a Haskell programmer so I might be wrong. Allowing arbitrary trait polymorphism is a feature with many use cases outside what this RFC proposes, such as a non-duck-typed write! macro and growable trait object pointers. (Yes, both of those examples could be made using the current proposal, but the former shouldn’t be restricted to only object-safe traits.)

1 Like

Could DownCastRef and UpCastRef be supplanted by implementing the regular DownCast/UpCaston references instead? Or would that introduce ambiguities when calling.up_cast()on a reference to a type that already implementsUpCast?

I don't think you can as the functions in the traits have different signatures.

Some questions about the proposal at the moment:

  • The need for offset in fat pointers is slightly confusing. Do you know any other examples of languages with fat pointers with offsets?
  • DynClass, DynRef and DynRefMut all appear to duplicate TraitObject. Why not extend TraitObject instead?

One potiential alternative to adding new casts to Rust is for the user to call the cast traits directly.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.