How to write a function that returns structs that implement a trait?

Hello,

for example I have these 3 structs:

pub struct Int{
    pub(crate) val: i32,
}

pub struct Plus<T:Exp>{
    pub e1: T,
    pub e2: T,
}

pub struct Mult<T:Exp>{
    pub e1: T,
    pub e2: T,
}

that implement this trait:

pub trait Exp{              
    fn eval(&self)->i32;
    fn pretty(&self)->String;   
}

but have a function like this:

 pub fn parse(&mut self) -> Option<Box<dyn Exp>>{
        let e = Parser::parse_e(self); //type Option<Box<dyn Exp>>
        return e;
    }

After I changed the return to Option < Box< dyn Exp > > I got an error saying I should implement
Exp for Box < dyn Exp >.

So I did only with default types to see if it gets that far at all:

impl Exp for Box<dyn Exp>{
    fn eval(&self) -> i32 {
        let x = 0;
        return x;
    }
    fn pretty(&self) -> String {
        let x = "here?".to_string();
        return x;
    }
}

Of course the parse function only returns this Box < dyn Exp > struct.
How can I specify that the return is one of those 3 structs that implement this trait?
I used Box because I read that it's not possible to determine the size at
compile time with Option < Exp >. Did I use it correctly?

I also found that it's possible to return an Option < impl Exp > .
Is that the way?

Given that the result of the parsing is not known (it could be an integer, a sum of expressions, or a product of expressions), you need to return one single type, at compile-time, which must be able to contain / branch / dispatch over these multiple possible implementors at runtime. That is, depending on your pattern, you'd want to say that you are returning...

  • an(y) Exp implementor, but where you don't know which one exactly: that choice shall be dynamic / depend on the value of runtime data (such as the parsed string). If the set of possible results is extensible (e.g., by downstream users), then you'd want to use a dyn Trait, such as a dyn Exp, which, when returned, needs to be Boxed (or Rc-ed): Box<dyn Expr>, which you can then freely wrap in an Option or a Result, or a Vec, or whatnot.

  • one of an integer literal, a sum of expressions, or a product of expressions exactly. The set of possible results is fixed / bounded, and you know all of the possibilities. In that case, the runtime / dynamic dispatch will just be branch-based, using an enum.

Given your use case, you should really go for the latter:

pub enum Expr {
    Int(Int),
    Plus(Plus<Box<Expr>, Box<Expr>>),
    Mult(Mult<Box<Expr>, Box<Expr>>),
}

pub struct Int{
    pub(crate) val: i32,
}

pub struct Plus<T: Exp> {
    pub e1: T,
    pub e2: T,
}

pub struct Mult<T: Exp> {
    pub e1: T,
    pub e2: T,
}

And at that point, the generics are now superfluous / redundant: the only type meeting the Exp contract will be that Expr (or Box<Expr> for technical reasons) disjonction of cases:

pub enum Expr {
    Int(Int),
    Plus(Plus),
    Mult(Mult),
}

pub struct Int {
    pub(crate) val: i32,
}

pub struct Plus {
    pub e1: Box<Expr>,
    pub e2: Box<Expr>,
}

pub struct Mult {
    pub e1: Box<Expr>,
    pub e2: Box<Expr>,
}

Congratulations, you now have an AST.

Note that you could flatten this data structure as:

pub enum Expr {
    Int {
        val: i32,
    },
    Sum {
        e1: Box<Expr>, // or `Box<Self>`,  it's the same, but showcases the recursive definition (which is why the `Box` or any other form of indirection is needed).
        e2: Box<Expr>,
    },
    // ...
}

which has its pros and cons, so it's not necessarily better (e.g., no privacy), nor worse (e.g., simpler). EDIT: heh, @steffahn and I posted this part at almost the same minute :laughing:


That syntax is to express a "third bullet" w.r.t. the two ones at the beginning of my post:

  • you'd be returning some implementor, unknown to the caller, but which you, on the other hand, know. Note that this implementor is a fixed type. You thus can't use impl Exp in return position to say that you will be returning two (or more) possible implementors of Exp. And since that's what you'd want to do when parsing runtime / dynamic data (an integer, or a sum, etc.), then -> ... impl Exp is not a good fit here :slightly_smiling_face:
5 Likes

If there's only these 3 possibilities for Exp, consider working with an enum instead of a trait. E.g.

pub enum Exp {
    Int {
        val: i32,
    },
    Plus {
        e1: Box<Exp>,
        e2: Box<Exp>,
    },
    Mult{
        e1: Box<Exp>,
        e2: Box<Exp>,
    },
}

well, now @Yandros finished their response as-well, right when I clicked the "reply" button to finish mine :smile:

4 Likes

Thank you @Yandros and you too @steffahn!
I'll try the latter branch-based option. It's more clear that way to see the ast-like structure in the enum.
And thanks for the advice how to flatten it, I didn't know that yet :slight_smile:

It was really close; when I hit "Reply" in my browser my answer was even displayed above yours for a moment.

1 Like

Hello again,

so I tried the branch-method but I'm a bit confused.
Is it correct, that the return type of functions that return one of the Exp variants has to be:

pub fn parse(&mut self) -> Option<Box<Exp>>

And before I implemented the trait containing the pretty() and eval() functions for each struct. So now with the enum Exp I thought I could do some pattern match like this:

impl Exp {
    pub fn eval(self) -> i32 {
        return match self {
            Exp::Int => { // and here the specific returns 
            },
            Exp::Plus => {
            },
            Exp::Mult => {
            }
        }
    }

    // same for pretty()
}

or implementing same-named functions for each struct:

impl Int{
    pub fn eval(&self) -> i32 {
    }
    fn pretty(&self)->String{
    }
}

impl Plus{
     pub fn eval(&self) -> i32 {
        self.e1.eval(&self) + self.e2.eval(&self) // ERROR: method eval() not found in Box<Exp>
    }
    fn pretty(&self)->String{
    }
}

//impl Mult

but I get a lot of errors:
For the first try, errors saying it expected a unit struct, unit variant or constant and found tuple variant Exp::Int, Exp::Plus, ...

then I tried changing that to:

impl Exp {
    pub fn eval(self) -> i32 {
        return match self {
            Int(_Int) => {
                self.val
            },
            Plus(_Plus) => {
                self.e1.eval() + self.e2.eval()
            },
            Mult(_Mult) => {
                self.e1.eval() * self.e2.eval()
            }
        }
    }

but now it says: expected tuple struct or tuple variant, found struct Plus and of course that the fields aren't found (which I understand because self refers to the Exp and not the variants containing the actual fields).

For the second try, errors saying the methods are not found in Box. Which sounds like the same issue like before. That now there has to be an impl of eval() and pretty() for Box.

What am I doing wrong?

The match patterns are not correct; to see why, the key observation is that patterns have a very similar shape to constructing a value.

For instance, if you wanted to construct an Integer Expr, you would do:

  • "Packed" / flattened enum case

    let my_val = 42;
    let expr = Expr::Int { val: my_val };
    

    well, then, assuming you no longer have access to my_val, only to expr, to match on it and get my_val back you'd write a pattern very similar to that of the construction:

    match expr {
       //   *if* in the `Int` variant…
       //   vvvvv
        Expr::Int {
            // matching the innards…
            val: /* let */ myval,
        } => {
            // You now have access to `myval` within this branch
            …
        },
        …
    }
    
  • "Unpacked" enum case:

    let my_val = 42;
    let expr = Expr::Int(Int { val: my_val });
    

    And to pattern-match-destructure it:

    match expr {
        //  if in the `Int` case
        //  vvvvv
        Expr::Int(Int {
            // matching the innards…
            val: /* let */ my_val
        }) => {
            // You now have access to `my_val` within this branch.
        },
        …
    }
    

So, similarly for Plus, you'd have, in the packed case:

impl Expr {
    fn eval(self: &Expr) -> i32 {
        match self {
            Expr::Int { val: my_val } => my_val,
            Expr::Plus { e1: my_e1, e2: my_e2 } => {
                my_e1.eval() + my_e2.eval()
            },
            Expr::Mult {
                // we could also name our vars `e1` and `e2`
                // rather than `my_e1` and `my_e2` to be shorter:
                e1: e1,
                // And there is even a shorthand syntax for this!
                e2,
            } => {
                e1.eval() + e2.eval()
            },
        }
    }
}
  • Feel free to try and write the "unpacked" enum case as an exercise :slightly_smiling_face:

Now, the unpacked case also has a very interesting aspect: it allows for a more modular approach, whereby you can extract the logic of each of these right-hand-side operations into the specific Int, Plus, and Mult types, and then have the top-level syntax just dispatch to those:

struct Plus {
  e1: Box<Expr>,
  e2: Box<Expr>,
}

impl Plus {
  fn eval (self: &Plus) -> i32 { // <-------------------------+
    let (e1, e2) = (&self.e1, &self.e2);                   // |
    // or, equivalently:                                   // |
    let Plus { e1: /* let */ e1, /* let */ e2 } = self;    // |
    e1.eval() + e2.eval()                                  // |
  }                                                        // |
}                                                          // |
                                                           // |
impl Expr {                                                // |
  fn eval (self: &Expr) -> i32 {                           // |
    // *dispatch* the `.eval()` call                       // |
    match self {                                           // |
      // Another possible construction of `expr`:          // |
      // let plus = Plus { … };                            // |
      // let expr = Expr::Plus(plus);                      // |
      Expr::Plus(/* let */ plus) => plus.eval(), // ----------+
      Expr::Int(int) => int.eval(),
      Expr::Mult(mult) => mult.eval(),
    }
  }
}
4 Likes

Thank you again :slight_smile: it works now

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.