"True" genericity

Hi there!
I'm currently developing a frontend framework that aims to use the MVU (Model - View - Update) similar to the architecture Elm brought to the web a few years ago.

For that, applications are split into pages, which are structures that implement the following trait:

// Simplified version of the actual trait
trait Page {
  type Msg;

  fn update(&mut self, message: Self::Msg);
  fn render(&self) -> VDOM<Self::Msg>;
}

The point is, I want developers to be able to associate routes (let's say these are just URL strings) to pages. And that's where I struggle.

My first attempt was to made something like:

let pages: HashMap<String, Box<dyn Page>>;

But the compiler complained about the fact I did not specified the associated Msg type.

Still, I do want to be able to store pages of different types.

One approach could be to replace Page by std::any::Any like this: HashMap<String, Box<dyn std::any::Any>>. But then there would be no guarantee the map's values are Pages.

And I cannot use HashMap<String, Box<dyn Page<Msg=Box<dyn std::any::Any>>> either, becaus in such case this will only accept pages that take messages of the Box<dyn std::any::Any> type, not of any type.

So, I wonder if there is a solution to this problem, using an approach I didn't think about, or if it's just impossible?

Thanks for your help! :slight_smile:

How would the code that uses pages know what type the Msg is? Without knowing what type Msg is, it is impossible to call either update or render.

That's the second part of the problem, indeed. When the framework receives a message through DOM events, as it may be of any type it is typed as an Box<dyn std::any::Any>. Then I though about the trait page implementing the following method:

trait Page {
  // ...
  fn downcast(message: std::any::Any) -> Option<Self::Message> {
    message.downcast_ref::<Self::Message>::()
  }
}

(And indeed I made a mistake in the trait I posted earlier, update does not take a Self::Msg but a &Self::Msg)

I solved the current problem by adding a second trait: (though surely your adventure here is far from complete; there's no telling what problem you will run into next!)

I missed the bit about &Self::Msg, so this uses boxes.

#[derive(Debug)]
pub struct MsgError;

pub type DynMessage = Box<dyn Any>;

pub trait DynPage {
    fn update_dyn(&mut self, message: DynMessage) -> Result<(), MsgError>;
    fn render_dyn(&self) -> Vdom<DynMessage>;
}

This trait is impemented by all Pages.

impl<T: Page> DynPage for T {
    fn update_dyn(&mut self, message: DynMessage) -> Result<(), MsgError> {
        match message.downcast() {
            Ok(downcast) => Ok(self.update(*downcast)),
            Err(_) => Err(MsgError),
        }
    }
    
    fn render_dyn(&self) -> Vdom<DynMessage>
    { Vdom(Box::new(self.render().0)) }
}

(note: when you do this, you have to add a 'static bound to type Msg. This is necessary so that it can implement Any. Alternatively, you can add where <T as Page>::Msg: 'static to the above impl)

If need be, you can then impl the original Page trait for Box<dyn DynPage>, with Msg = DynMessage.

impl Page for Box<dyn DynPage> {
    type Msg = DynMessage;
    
    fn update(&mut self, message: Self::Msg)
    { self.update_dyn(message).unwrap() }
    
    fn render(&self) -> Vdom<Self::Msg>
    { self.render_dyn() }
}

Full playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=badabec5262a602116254a600dfb1fa0

1 Like

Oh that's pretty clever! I already thought about making update take a Box<dyn std::any::Any> value instead of a Self::Message one and make it do the downcast itself, but that was a bit ugly - I didn't thought about splitting it into two traits.

Thanks a lot for your answer, that solves my problem! :smiley:

If you're ok with updating pages being fallible in general you can do something similar without an extra trait, just defining an alias for a dynamic Page and a small shim to take a static Page and make it dynamic: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=aec6e60209c058834809287bd8201d52 (also needs a dynamic shim for Box<Error> as that doesn't implement Error :frowning:).