Allow generic method on dyn objects is possible?

If we add dyn modifier on type parameters to mark them as to be erased, could universal types able to be implemented?

trait Foo {
  fn foo<dyn T>(self, vec: DynVec<T>) -> i32;
} 

struct bar {}

impl Foo for bar {
  fn foo<dyn T>(self, vec: DynVec<T>) -> i32 {
    return vec.len()
  }
}

struct DynVec<dyn T> {
   vec: Vec<*const ()>,
  // as T is a dyn parameter, we don't need PhantomMarker
}

impl<dyn T> DynVec<T> {
  fn new() -> DynVec { ... }
  fn len(&self) -> i32 { self.vec.len() }
}

impl<T> DynVec<dyn T> {
  fn push(val: T) {
   let b = Box::new::<T>(val);
    self.vec.push(b.into_raw() as *const ());
  }
}

No, it's impossible for dyn Trait due to object safety.

Dispatchable functions require:

  • Not have any type parameters (although lifetime parameters are allowed)

I mean a way no to violate object safety but also allow us to use generic types in dyn objects. (so we
can implement universal types and existential types).

Because dyn T will be erased, so

struct DynVec<dyn T> {...} 

will be de-suger to

struct DynVec { vec: Vec<*const ()> }

A dyn-type parameter will only be used for type checking as the lifetime specifier.

You can always use type parameters on traits in trait objects: Rust Playground

trait Trait<T> {
    fn f(&self) -> T; // object safe and dispatchable: no type paramters on methods
}

No, I mean a value which is still a polymorphic type, so universal types and existential types can be implemented in rust.

for example:

trait Identity {
  fn id<dyn T>(&self, t: *T) -> *T
}

fn foobar<dyn T>(id: &dyn Identity, ptr: *T) -> *T {
  id.id(ptr)
}

Most of the time we can implemented the logic by Trait which dispatch statically, but sometimes maybe we want to save different types which shard behaviour in a same container.

Something like this would be nice to have, but it does have lot of complications, for example:

This function can be polymorphic under the assumption that all *T have the same representation, indipendently from what T is, but this is false since we can have fat pointers.

I guess it means you want trait objects in trait methods. Since trait objects are not generic types, they are concrete types instead, so today these are valid in Rust:

trait Identity1 {
  fn id(&self, t: Box<dyn Trait>) -> Box<dyn Trait> {t}
}

trait Identity2 {
  fn id<'t>(&self, t: &'t dyn Trait) -> &'t dyn Trait {t}
}

If you want to combine the two cases by using pointers, it's ok

trait Identity3 {
  fn id(&self, t: *mut dyn Trait) -> *mut dyn Trait {t}
}

If you don't want the fat pointer represented by PointerType<dyn Trait>, the unstable dyn* Trait type (thin pointer) is at your choice. Rust Playground

Here *T is just *const T or *mut T, where they can be saved as *const () or *mut () and cast back when there is enough statical information about the actual type. I just use this as an example, because lifetime will be the actual hard thing. And for fat pointers we can also try to use some wrapper to warp them in the same shape box.

No, this is not something I'm talking about. For example you can't guarantee Identity would return the same underlaying shape behind Box.

fn identity(x: Box<dyn Trait>) -> Box<dyn Trait> {
  return Box::new(TraitObject2{
    ...
  })
}

trait Trait: Any {}

let x = Box::new(TraitObject1{
   ...
});
let y = identity(x);
assert_eq!(x.type_id(), y.type_id()); // failed

Actually, your code snippet is very confusing, though I didn't point it out from the start:

dyn T means dyn Trait right? Then DynVec<T> is DynVec<Trait>, but you write

I know dyn Trait and Trait here can be one thing in early edition, but this line

*T should mean *Trait which is not valid at all.

You're mixing Trait and Type along the way :sweat: Not to mention <dyn T> is neither valid.

1 Like

So you want these methods to:

  • in regards for typechecking, behave as if they were generic (in particular, ensure that a single parameter corresponds to a single arbitrary type);
  • in regards for codegen, behave as if they were using dyn Trait (in particular, generate only one version, without monomorphization for every possible type).

Is this right?

I think you have misunderstood. I'm not talking about current language feature. I'm talking about adding dyn modifier to type parameter. You can read my post from the start.

You can see here the keyword dyn is a modifier over a type parameter T, not a trait.

trait Foo {
  fn foo<dyn T>(self, vec: DynVec<T>) -> i32;
} 

It's used to indicate that for a given type constructor T :: * -> * or a polymorphic value X :: * -> Y, the representation doesn't depends on the type parameter, so compiler can erase type parameter from the expression. This will allow us to implement universal types and existential types.

Almost, but I'm not sure about the second part. Because in Rust we don't have subtyping so we don't need a F-sub system. My original thought is just find a single representation so we can perform type erasure.

For example, currently we can write

trait Counter {
  fn count<T>(self, &Vec<T>) -> usize;
}

fn count<C: Counter, T>(c: C, vec: &Vec<T>) -> usize {
  c.count<T>(vec)
}

but count must be specilized during compile time. We can't write:

fn count<T>(f: &dyn Fn<T>(&Vec<T>) -> usize, vec: &Vec<T>) -> usize {
  f<T>(vec)
}

It's not possible to specilize f for every T and dispatch by type at runtime.
But if we can know f only have single representation, then we only need to use a vptr point to that representation and call it. So we can write:

fn count<T>(f: &dyn Fn<dyn T>(&Vec<DynBox<T>>) -> usize, vec: &Vec<DynBox<T>>) -> usize {
  f(vec)
}

In the above codes, f should have a single representation, so we don't need any specilization. This will be useful when writing some virtual machine codes.

Your examples include taking things by value, which naturally does not permit a single representation (as I'm interpreting your terminology (layout basically); could be a misunderstanding).

Rust does have subtypes (higher-ranked types and types with covariant and/or contravariant lifetimes).

2 Likes

Yes Rust don't have subtyping which make things more easier, we only need a subset of system F rules, for those type which only need to be single represented. For example

struct DynBox<T> {
  ptr: *mut T
}

This struct only needs to be compiled into one single shape for every T.

It does.

Perhaps not currently a blocker for whatever you want, but it exists. Rust may gain types higher ranked over types in addition to lifetimes, which might also be relevant.

For every Sized type, yes. The same cannot be said for this method that takes the type by value.

Then IRLO is probably a more appropriate forum. Though I don't know if things will go very far without a big step up in communication (based in a Rust perspective, vs. a system F perspective).

1 Like

For lifetime, surely. Because I want to discuss tranditional subtyping over shapes of objects, so I excluded lifetime in this part. And Rust already implemented type reconstruction algorithm for lifetime, I think it should be easy to integrate other subtyping support(althrough subtyping is a controversial thing indeed).

For this code you can't make it working for dyn T

impl<T> DynVec<dyn T> {
  fn push(val: T) { ... }
}

But you can box it first, where boxing happened in the context which knows the actual type of T(a non-dyn context).

impl<T> DynVec<dyn T> {
  fn push(val: DynBox<T>) { ... }
}

fn demo() {
  let b = DynBox::new::<i32>(1);
  let vec = DynVec::new::<i32>();
  vec.push(b);
}

I'll try to start a thread in the IRLO, thank you.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.