Code composition in Rust

Hey.

I'm having trouble maintaining code composition in Rust. Every time I get an idea, the compiler effectively rules out each of my approaches.

My intention is as follows, I have the following trait:

#[async_trait]
pub trait Fetchable {
    fn can_fetch<S: Into<String>>(&self, link: S) -> bool;
    async fn fetch<S: Send + Into<String>>(&self, link: S) -> Result<Bytes, Box<dyn Error>>;
}

Then I have a structure that holds a vector of these traits and based on the "can_fetch" method selects the first available one, the issue is really simple but the compiler spits out errors. My struct is following:

pub struct FetcherProvider {
    fetchers: Vec<Box<dyn Fetchable>>
}

impl FetcherProvider {
    pub fn new(fetchers: Vec<Box<dyn Fetchable>>) -> FetcherProvider {
        FetcherProvider{fetchers}
    }

    pub fn get_fetcher<S: Into<String>>(&self, link: S) -> Result<Box<dyn Fetchable>, ()> {
        for fetcher in self.fetchers.into_iter(){
            if fetcher.can_fetch(link.borrow()) {
                return Result::Ok(Box::from(fetcher));
            }
        }
        return Result::Err(());
    }
}

Error is following:

error[E0038]: the trait `fetcher::Fetchable` cannot be made into an object
  --> src/fetcher.rs:15:15
   |
15 |     fetchers: Vec<Box<dyn Fetchable>>
   |               ^^^^^^^^^^^^^^^^^^^^^^^ `fetcher::Fetchable` cannot be made into an object
   |
   = help: consider moving `can_fetch` to another trait
   = help: consider moving `fetch` to another trait
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src/fetcher.rs:10:8
   |
9  | pub trait Fetchable {
   |           --------- this trait cannot be made into an object...
10 |     fn can_fetch<S: Into<String>>(&self, link: S) -> bool;
   |        ^^^^^^^^^ ...because method `can_fetch` has generic type parameters
11 |     async fn fetch<S: Send + Into<String>>(&self, link: S) -> Result<Bytes, Box<dyn Error>>;
   |              ^^^^^ ...because method `fetch` has generic type parameters


What is the proper approach for such case? I checked the link that appears in the compiler error, but I still don't see the correct path here.

you cannot have generic methods in trait objects

trait Foo {
    // not allowed if you want `Foo` to be object safe
    fn foo<T: Into<String>>(&self, _: T); 
}

what you can do is add another trait:

trait IntoString {
    fn into_string(self: Box<Self>) -> String;
}

trait Foo {
    fn foo(&self, _: Box<dyn IntoString>);
}

Thank you, this seems to solve my problem. However, I am still wondering if there is any other solution that does not require creating a new trait? The situation where I want to allow parameters of type &str or String seems to be quite trivial to me, hasn't it been done out-of-the-box in Rust already?

if you want just to be able to use &'a str or String you can maybe use Cow<'a, str>.

then you can call can_fetch like this:

foo.can_fetch("hello world".into()); // this doesn't allocate
foo.can_fetch(String::from("baz").into());
1 Like

A function being generic over &str or String is an ergonomic convenience; it doesn't make the function itself more flexible, because you can always borrow a &str if you have a String and you can always turn a &str into a String. So, when the generic version is not possible, you should simply accept the type you need and let the caller figure out how to get it.

This means that if you need a String and will use it as a String internally, just accept link: String and let the caller call .to_owned() or .into() if they need to. On the other hand, if you only need a &str and will not need to convert it to the owned version internally, just accept &str and let the caller borrow if they have a different type.

Since this is a trait, if different implementors require different things – say one implementation may require String, but another one can make do with &str – it's up to you what to do: either option will require unnecessary copies in some case. (But note that this is actually no different than the generic version.) Using Cow<str> lets you provide only what you have, and the caller use only what it needs, at a slight runtime cost, so that is the third option.

2 Likes

Thank you, exactly the description I needed. As I wrote in the topic, I'm having trouble maintaining the composition in this language.

What I've described here is a very simple behavior, I'm still worried that with a larger project I'll run into a similar problem that will require drastic changes to the entire project architecture. This is the main problem why I can't implement this language in production applications.

It's a struct containing a vector of async trait objects... sure, it might not be an abstract singleton proxy factory whatever, but I think "very simple" might be overstating it just a little.

Yes, you should anticipate drastic changes to project architecture when changing the implementation language, at least if you want to end up with something better and not worse. If you're not prepared to go back to the drawing board, you can't correct the architectural problems that cause unsafety: unclear ownership, ambiguous lifetimes, race conditions, etc.

That said, this issue doesn't seem to have anything in particular to do with unsafety and is easy enough to work around by creating a new non-object-safe trait to contain the generic items:

#[async_trait]
pub trait FetchableCore {
    fn can_fetch_inner(&self, link: String) -> bool;
    async fn fetch_inner(&self, link: String) -> Result<Bytes, Box<dyn Error>>;
}

#[async_trait]
pub trait Fetchable: FetchableCore {
    fn can_fetch<S: Into<String>>(&self, link: S) -> bool { self.can_fetch_inner(link.into()) }
    async fn fetch<S: Send + Into<String>>(&self, link: S) -> Result<Bytes, Box<dyn Error>> { self.fetch_inner(link.into()) }
}

impl<F: FetchableCore + ?Sized> Fetchable for FetchableCore {}

(I'm not sure if async_trait messes this up at all; I think it should work)

For prior art, consider rand's Rng and RngCore traits.

Rust is (IMO) well-suited to big, complex systems. It's definitely not suited to porting existing solutions from other languages, type for type, idiom for idiom, abstraction for abstraction.

3 Likes

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.