How should one deal with Boxed trait objects in Rust?

Viewed 228

I have a trait Expression which is defined on multiple types. A typical pattern I tend to include in my types is Box<dyn Expression>. Here's an example:

pub struct BinaryExpr {
 l : Box<dyn Expression>, r : Box<dyn Expression>
}

I have another trait ExpressionPrinter which is defined like :

pub trait ExpressionPrinter: Expression {
 fn print(&self); 
}

As you can see, it takes a immutable reference to self, and is supposed to print a string representation of the any expression type implementing it. Let's say I was implementing this for BinaryExpr


impl ExpressionPrinter for BinaryExpr {
    fn print(&self)  {
        let mut s = start!("BinaryExp");
        s.push_str(&self.operator.lexeme);
        s.push_str(&self.left.print());
        s.push_str(&self.right.print());
        dbg!(s);
    }
}

This code does not compile because : enter image description here

Alright, maybe I need to deref from Box before calling the methods, but wait, isn't Box supposed to auto-deref? Then I looked around the docs for Box realizing I don't really understand Boxed trait objects. There's an into_inner but it doesn't work as the inner trait object is !Sized. There's a downcast which

Attempt to downcast the box to a concrete type.

But it requires trait Any to be implemented for each expression type.

Needless to say, this abstraction is not fun anymore with so many restrictions. I don't understand how to call trait object methods.

EDIT: The question as it stands now is how do I cast from a Box<dyn Expression> to a concrete type bounded by ExpressionPrinter?

2 Answers

What you are doing is not how you implement AST in rust. This is not java or other strongly OOP language. What you should use are Algebraic types (enums). These types allow to store multiple memory layouts inside one type. Here is an example:

pub enum Expr {
    Int(i64),
    String(String),
    Binary {
        op: String,
        left: Box<Expr>,
        right: Box<Expr>,
    }
    Unary {
        op: String,
        operand: Box<Expr>,
    }
}

Enum enables you to store all this values in same variable or vector and when you need to figure out which variant it is you use match statement like so:


let op = match &expr {
    Expr::Binary { op, .. } | Expr::Unary { op, .. } => op,
    _ => unreachable!("Operator is expected."), 
}

In your example ExpressionPrinter is a sub trait of the super trait Expression. This means that any type that implements the sub trait must also implements the super trait. It's not clear from your example if BinaryExpr implements both traits or only the sub trait ExpressionPrinter, which is the first mistake.

The second mistake is that you attempt to invoke print from something that implements Expression trait which the compiler cannot know.

You may try this, which is not nice:

pub struct BinaryExpr {
 l : Box<dyn ExpressionPrinter>, r : Box<dyn ExpressionPrinter>
}

impl ExpressionPrinter for BinaryExpr {
    fn print(&self)  {
        let mut s = start!("BinaryExp");
        s.push_str(&self.operator.lexeme);
        // Now the compiler knows that the left and 
        //right have a print method because it's 
        // something that implements ExpressionPrinter
        s.push_str(&self.left.print());
        s.push_str(&self.right.print());
        dbg!(s);
    }
}
Related