Genericity over traits

(Sorry if this has been discussed before, this combination of keywords obviously returns a lot of results about existing features)

I’ve sometimes needed a function to be generic over a trait, rather than a type. One use case is transmuting a trait object reference to a std::raw::TraitObject, in a generic fashion.

fn foo<T: ?Sized>(x: &T) -> TraitObject {
    transmute(x)
}

This doesn’t work as T could be sized, or another DST (slice or str). The compiler complains that x might have the wrong size (one word, rather than 2 for TraitObject). transmute_copy can be used as a workaround, but is very much UB if T is not a trait object.

My suggestion is to allow T to be restricted to a trait :

fn foo<trait T>(x: &T);

I think the syntax fits nicely with the various const x: u32 generics proposals. The keyword before the variable refers to the style (avoiding the words type and kind on purpose here) of variable, and can be either const, trait, type (the default) or an apostrophe for lifetimes.

fn foo<'a, trait T: Debug, type U: T, const x: u32>(data: [U; x]) {
    println!("{:?}", data);
}

(Just to be clear, this proposal is orthogonal to value generics proposed by RFC 1657 and others, the syntax just fits well)

Another example is a generic “constraint checker”:

fn check_is_trait<Trait, T: Trait>() {}

check_is_trait<Clone, u8>();
check_is_trait<Copy, String>();

instead of

fn check_is_clone<T: Clone>() {}
fn check_is_copy<T: Copy>() {}

check_is_clone<u8>();
check_is_copy<String>();

Another another example is dynamic code loading and objects, where you want to be able to do something like obj.cast_to::<Interface>().

These are all very different features from one another.

3 Likes

How? All three examples are doing the same thing as far as I can tell: being able to pass a trait as a generic parameter. They're just being applied to different problems.

They’re using the same syntax to mean three different and incomptible things.

  • The first is signalling that this type is a trait object.
  • The second is adding a new kind to Rust, which is a trait constraint, allowing you to parameterize types by a term which appears in a different position from lifetimes and types.
  • The third is (I believe) downcasting one trait object to another. This is similar to the first proposal but would also require additional RTTI if I understand correctly.

The first and second are definitely incompatible. I think the first, if we wanted to do it, is better solved by a new special marker trait like Virtual which all trait objects implement and no other types do.

1 Like

Just to elaborate on this because it took me a minute to realize why this is, the problem is that only some traits are object safe, so it's not generally true that an arbitrary trait could be used in a type position as an object type. A trait kind would therefore be orthogonal to a Virtual (or Object) bound applied to a type parameter.

[removed: Maybe if you could write fn foo<trait Trait: Virtual>(...), then Trait could be treated as a type as well as a bound, though this would require Virtual to be deeply understood by the compiler.]

Edit: Had the meaning of <trait T: U> wrong.

Its even more fundamental than that. For any object safe trait called Trait, there also exists a type called Trait which is the trait object for that trait. But one of these is a trait and the other is a type, so they are different things which have the same name.

For example, the syntax you just wrote - <trait T: Virtual> - doesn’t have a single obvious meaning. In particular, traits don’t implement other traits. So my inference, which may not be yours, is that its the same thing as trait Foo: Bar { }; this is called a “supertrait,” but all it really is is a trait constraint on the Self type for that trait (that is, no different from writing trait Foo where Self: Bar { }.

If this is what it means, its very different from what I wrote, because Virtual is implemented by every trait object, but its not a supertrait for every object safe trait. All of their non-trait object implementing types do not implement Virtual.

2 Likes

Right, that doesn’t make sense. I was trying to handwave and it backfired. You’d have to write fn foo<Trait: Virtual>(...) and then have the compiler somehow understand that Trait is dual-kinded. Or introduce an intersecting kind fn foo<trait+type Trait>(...), in which case, you could just define

impl<trait+type Trait> Virtual for Trait {}

in order to be able to get the basic “is an object type” bound.

I get that the object type and the trait are two separate entities in one sense, but they’re very closely related entities. If for whatever reason you wanted both, you’d need to be able to capture both in a single parameter in order to ensure that they’re interoperable. Though I’m not sure what such a case would look like.

Given the frequent confusion, was it ever suggested (for Rust 2.0?) to not give the trait object type the same name as the trait?

Yes, a lot of folks think the current syntax for trait objects is a mistake for various reasons.

2 Likes

Sorry for not noticing this earlier.

There is a way to do this in unstable Rust, and it has been there for a while. Some people have used them to build collections such as “ring buffer” double-ended queues of trait objects without indirection to the data.

You could use it with fn foo<T: Unsize<Trait>, Trait: ?Sized>, for example. The caveat is that it will actually work with all DSTs, not just trait objects, and there’s some subtlety here:

You can’t be generic over traits specifically, and there is no good reason for it. Unsafely accessing the vtable is unnecessary, given drop_in_place and {align,size}_of_val (and of course the actual virtual methods).

That means that a DstDeque<[T]> would also work if DstDeque<Trait> does, and it makes sense (“double-ended queue of slices”).

3 Likes

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