Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generalising over two structs

I have two independent libraries (distinct crates), each defining a struct and a function to build such a struct, if possible (for simplicity, I put everything into one module):

struct Foo {}
struct Bar {}

impl Foo {
    fn make_foo(a: &str) -> Option<Foo> {
        Some(Foo {})
    }
}

impl Bar {
    fn make_bar(b: &str) -> Option<Bar> {
        Some(Bar {})
    }
}

I now want to handle the two structs in the same way, independent of whether I actually have a Foo or a Bar:

trait SomeTrait {
    fn do_something() -> ();
}
impl SomeTrait for Foo {
    fn do_something() -> () {
        ()
    }
}
impl SomeTrait for Bar {
    fn do_something() -> () {
        ()
    }
}

Since the size of SomeTrait is unknown at compile time, I can't simply let a: Option<SomeTrait> = Foo::make_foo("abc"). So I tried wrap it in a Box (before unpacking the Option):

fn main() {
    let f: Option<Box<SomeTrait>> = Foo::make_foo("abc").map(Box::new)
}

But still the compiler complains:

error: mismatched types [E0308]
    let b: Option<Box<SomeTrait>> = Foo::new().map(Box::new);
                                    ^~~~~~~~~~~~~~~~~~~~~~~~
help: run `rustc --explain E0308` to see a detailed explanation
note: expected type `std::option::Option<Box<SomeTrait>>`
note:    found type `std::option::Option<Box<Foo>>`

I tried to use as for casting, but that didn't work since these are all non-scalar types.

How do I solve this (without touching the implementation of Foo and Bar)? I feel like I'm trying to apply patterns from classical OOP-languages to Rust's trait system.

In the end, I want to be able to do something like:

fn main() {
    let arg = 5;
    let x: Option<Box<SomeTrait>> = match arg {
        4 => Foo::make_foo("abc").map(Box::new),
        _ => Bar::make_bar("abc").map(Box::new),
    };

    x.do_something()
}

Full example:

// Library part
struct Foo {}
struct Bar {}

impl Foo {
    fn new() -> Option<Foo> {
        Some(Foo {})
    }
}
impl Bar {
    fn new() -> Option<Bar> {
        Some(Bar {})
    }
}

// My Part
trait SomeTrait {
    fn do_something(&self);
}
impl SomeTrait for Foo {
    fn do_something(&self) {
        println!("foo")
    }
}
impl SomeTrait for Bar {
    fn do_something(&self) {
        println!("bar")
    }
}

fn main() {
    let b: Option<Box<SomeTrait>> = match "x" {
        "x" => Foo::new().map(Box::new),
        _ => Bar::new().map(Box::new),
    };
    b.unwrap().do_something();
}
like image 770
jobach Avatar asked Sep 02 '25 04:09

jobach


1 Answers

There are two general problems with this. The first is you need to have the object represented as a Option<Box<SomeTrait>> so setting your variable binding needs to be something like this let f: Option<Box<SomeTrait>> = Some(Box::new(Foo::make_foo("abc")));

and you need to remove the option part from your make method so you can wrap it properly later.

impl Foo {
    fn make_foo(a: &str) -> Foo {
        Foo {}
    }
}

The ordering here is important. Option can't hold an unsized trait so it needs to wrap the box.

Second is, a trait can not become a trait object if the trait has methods that don't reference &self

A quick except from the rust error docs shows

Method has no receiver

Methods that do not take a self parameter can't be called since there won't be a way to get a pointer to the method table for them.

So for your trait methods you need

trait SomeTrait {
    fn do_something(&self);
}
impl SomeTrait for Foo {
    fn do_something(&self) {  }
}
impl SomeTrait for Bar {
    fn do_something(&self) {  }
}

Edit:

Since you can not change the make functions, another approach for mapping them properly is adding a return type to the lambda so it knows how to help the compiler agree with the type.

let f: Option<Box<SomeTrait>> = Foo::make_foo("abc").and_then(|foo| -> Option<Box<SomeTrait>> {
    Some(Box::new(foo))
});
like image 75
Eric Y Avatar answered Sep 05 '25 02:09

Eric Y