Reconsider trait as another

Hi !

I define a trait CityTask which is depending on another trait, Task: CityTask: Task. How can I use objects implementing CityTask where are expected Task ?

There is an example (where I added some things like Send, Sync, etc to be closer than my "real" code):

trait Task {
    fn id(&self) -> usize;
}

trait CityTask: Task {
    fn foo1(&self);
}
trait UnitTask: Task {
    fn foo2(&self);
}

type TaskBox = Box<dyn Task + Send + Sync>;
type CityTaskBox = Box<dyn CityTask + Send + Sync>;
type UnitTaskBox = Box<dyn UnitTask + Send + Sync>;

fn build_some_city_tasks() -> Vec<CityTaskBox> {
    todo!()
}

fn work_with_any_tasks(tasks: Vec<TaskBox>) {
    todo!()
}

fn main() {
    let tasks = build_some_city_tasks();
    work_with_any_tasks(tasks);
}

Which produce the following error:

error[E0308]: mismatched types
  --> src/main.rs:26:25
   |
26 |     work_with_any_tasks(tasks);
   |     ------------------- ^^^^^ expected trait `Task`, found trait `CityTask`
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected struct `Vec<Box<(dyn Task + Send + Sync + 'static)>>`
              found struct `Vec<Box<dyn CityTask + Send + Sync>>`
note: function defined here
  --> src/main.rs:20:4
   |
20 | fn work_with_any_tasks(tasks: Vec<TaskBox>) {
   |    ^^^^^^^^^^^^^^^^^^^ -------------------

I think it should be possible because CityTask is depending on Task ... But, I don't know how to deal with that.

Using the nightly feature #![feature(trait_upcasting)], you can simply write this:

let tasks = build_some_city_tasks();
work_with_any_tasks(tasks.into_iter().map(|x| x as TaskBox).collect());

See also:

2 Likes

I've been playing around with the same idea for a few minutes now, and I'm a bit disappointed about the assembly when panic=abort isn't set :-/ (even though at least it still seems to have removed the main loop, but some destructor calls remain which will end up doing nothing)

rust subtrait is not inheritence/subtyping (so called is-a relation) of typical object-oriented languages.

in rust, a type implementing a subtrait can call methods defined in its supertraits. however, direct trait upcasting is still experimental.

to be honest, many "conventional" OOP patterns are not easy to express in rust, but I believe many of those "legacy" patterns are not needed in the first place, if your language have an advanced/sophisticated type system.

Hopefully not for too long anymore.

We almost had it stable last Feburary already…

I might have had a little bit to do myself with finding an issue that motivated the revert to leave time for fixing some issues :sweat_smile:

(I wouldn't have anticipated that the whole process would delay it by over year though)


That is true. While trait upcasting is designed to be cheap if possible, as soon as you have multiple supertraits next to each other, it might no longer be a no-op in the current implementation (when the vtable pointer needs adjusting), in which case doing it behind another level of indirection (in a Vec) can't be an implicit cast.


That being said, I personally wouldn't be surprised if we got there eventually; some sort of combination of

  • allowing some marker on (at most) one supertrait, to allow the API to stably promise no-op convertibility to that supertrait
    • (or perhaps even an automatism; that's a design question around whether there are any semver-compatibility concerns)
  • some framework of extended subtyping&variance / or a separate system of safe transmutations, to lift such no-op-convertibility properties through containers like Vec<_>

I feel like, with Rust's standing regarding performance & safety, it seems almost certainly a desireable enough feature to make happen eventually.

3 Likes

I was not expecting a so interesting discussion ! Thanks to all. I'm reading all of that :slight_smile:

1 Like

You do not need trait upcasting in order to make this code work; you can instead make work_with_any_tasks generic:

fn work_with_any_tasks<T: ?Sized + Task>(tasks: Vec<Box<T>>) {
    todo!()
}

It's not currently possible to upcast dyn CityTask to dyn Task, but dyn CityTask still implements Task.

I would also generally recommend adding implementations of your traits for Box,[1] when possible; if you do this, then you can make the function simpler and even more generic, and allow it to be used without boxing and dyn when that suits the situation:

impl<T: ?Sized + Task> Task for Box<T> {
    fn id(&self) -> usize {
        (**self).id()
    }
}

fn work_with_any_tasks<T: Task>(tasks: Vec<T>) {
    todo!()
}

  1. and & and Rc and Arc ↩︎

2 Likes

I agree with @kpreid that there is no actual need for trait upcasting in the example. Moreover, despite the naming, the transformation from dyn SubTrait to dyn SuperTrait is a coercion, not an actual type upcast -- there is no sub/supertype relation between the two dyn types. Which is why even when you have trait upcasting, you need to create a new Vec as in @nerditation's example. So (given what's been shared), I think you're better off without it.

All that being said, if you still want it for some reason, you can write your own upcasting methods on stable.

1 Like