Why my dynamic dispatch won't work?


#1

I am trying to make a ski calculus simulation system. I want that user can extend their own combinator. Here is my implementation. I delete many unrelated code to emphasize my problem.

// src/ski.rs

pub enum SKI {
    SKISymbol,
    SKICombinator(Box<SKICombinator>)
}

// src/skicombinator.rs

pub trait SKICombinator {}
pub struct S { }
impl SKICombinator for S {}

// src/print.rs, I defined new property here, I use print as example here

pub trait Print {
    fn print (&self)
}

impl Print for SKI {
    fn print(&self) {
        match *self {
            SKI::SKISymbol => println!("skisymbol"),
            SKI::SKICombinator(ref c) => c.print(),
        }   
    }   
}

impl Print for SKICombinator {
    fn print(&self) { unreachable!() } // have to do this if I want to call .print()
}

impl Print for S {
    fn print(&self) { println!("S") }
}

// main.rs

pub fn main() {
    let a = SKI::SKICombinator(Boc::new(S{}));
    a.print();
}

I execute it and I hit into unreachable.
I supposed dynamic dispatch should work and it will print “S”. Can anyone help me?


#2

You’re not using dynamic dispatch. You’re invoking Print::print on an SKICombinator… so it calls the Print::print that you defined for SKICombinator.

I assume you wanted to call Print::print for S. You can’t do that, because you have a Box<SKICombinator>, which only allows you to call methods defined in the SKICombinator trait. There is no print method defined. In fact, SKICombinator has no methods, so there’s nothing you can call on it.

If you want to do dynamic dispatch through SKICombinator, you need to define the methods in the trait itself. Also note that Rust does not have inheritance, so writing

pub trait SKICombinator: Print {}

won’t work. Rust also doesn’t have dynamic casting, so you can’t “cross cast” from SKICombinator to Print.

One thing you can do is add something to SKICombinator directly to do casting:

pub trait SKICombinator {
    fn as_print(&self) -> Option<&Print> { None /* by default */ }
}

If an implementation of SKICombinator implements Print, it can then also implement as_print to give callers access to it.


#3

Thank for the reply.

Just some following question:

In my example, the s.print() can be called. and it will go to unreachable!.

When I write

impl Print for SKICombinator

I actually add a method into the trait, that is what I thought. So when I call print() it should goes into the Box<SKICombinator> and found it is a S{} instance. Or maybe something is wrong?

And just to make sure that, what I do is impossible … right? If I defined a trait and later want to extend it for other method without modify the trait implementation. Each instance can make their own version of print and use dynamic dispatch on the Enum that has Box<Trait>.

Maybe I should say more detail so you can help me better. I make the SKI enum and SKICombinator trait. User can define their combinator if they liked.
Now I need to implement a method called transform to translate SKI calculus to another type of calculus. The transform (print in my example) method need to go through the tree and called transform on each node.
At the node of SKI::SKICombinator. Since different combinator will return different result. It also has to invoke the method defined in each instance, which I failed to implement here.


#4

No, you implemented Print on the trait. For dynamic dispatch, you want to implement methods on the underlying type, which are then called via the trait. This is implementing a method on the trait, which is then called directly via static dispatch.

Implementing a trait directly for another trait is almost never what you want.

Again, if you want to be able to call a method through a Box<Trait> (or &Trait, Rc<Trait>, etc.), you can only do so via methods defined in that trait. You’ll need to put print (or an as_print, or whatever you need) in the SKICombinator trait directly.


#5

OK, I understood, thanks.