Richard Neumann
Richard Neumann

Reputation: 3361

Implement trait that has function which return traits

I am trying to implement a trait for a struct which in turn has functions that return traits. I want this, because I do not want to bind the uer to a specific data structure. However, trying to apply the compiler's correction suggestions, I fell deeper and deeper into a rabbit hole to no avail. Here's a minimal example of what I'm trying to do:

trait WordsFilter {
    fn starting_with(&self, chr: char) -> dyn Iterator<Item = String>;
}

struct WordsContainer {
    words: Vec<String>,
}

impl WordsFilter for WordsContainer {
    fn starting_with(&self, chr: char) -> dyn Iterator<Item = String>
    {
        self.words.iter().filter(|word| word.starts_with("a"))
    }
}

fn main() {}

Which results in:

error[E0277]: the size for values of type `(dyn Iterator<Item = String> + 'static)` cannot be known at compilation time
  --> .\traits.rs:10:40
   |
10 |     fn starting_with(&self, chr: char) -> dyn Iterator<Item = String>
   |                                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `(dyn Iterator<Item = String> + 'static)`
   = note: the return type of a function must have a statically known size

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

I tried to apply the compiler's correction's step by step but they were just getting more.

Upvotes: 1

Views: 860

Answers (2)

Michael Anderson
Michael Anderson

Reputation: 73470

There are several things that need to be fixed to get this to work.

I'll first show the workng version then step through what had to change and why.

trait WordsFilter {
    fn starting_with(&self, chr: char) -> Box<dyn Iterator<Item = String> + '_>;
}

struct WordsContainer {
    words: Vec<String>,
}

impl WordsFilter for WordsContainer {
    fn starting_with(&self, chr: char) -> Box<dyn Iterator<Item = String> + '_>
    {
        Box::new(self.words.iter().filter(|word| word.starts_with("a")).cloned())
    }
}

fn main() {}
  1. Boxing the iterator. Rust doesn't allow you to return naked dyn types. The compiler error tells you to Box it. So I did.

  2. Clone after filter. Vec<X>::iter() returns an iterator with Item=&X so in this example we get Item=&String we want Item=String so we need to clone.

  3. Add a lifetime. The problem was that without the lifetime checks I could write this...

    let words_container = WordsContainer{...};
    let it = words_container.starting_with();
    drop(words_container)
    it.next()
    

    but it.next() is stepping through the internal vector that is inside words_container - which no longer exists.

    Adding the '_ lifetime on the trait is saying the iterator is only valid for as long as the underlying container is. So now the above code would generate a compile error.

Upvotes: 1

Aleksander Krauze
Aleksander Krauze

Reputation: 6061

TL;DR Use generics or associated types.

You cannot use bare dyn Trait types. They are Unsized which means that compiler cannot know their size at the compile time. Therefore you can only use them behind some reference (similarly to how you cannot use str or [T] and must use &str and &[T]). The easiest way is to use Box<dyn Trait>, like PitaJ earlier suggested. This requires of course one heap allocation, but allows a very nice API.

In normal functions and methods you can however use impl Trait trick to make compiler infer returned type. This is used like this

fn foo() -> impl Iterator<Item = ()> {
    todo!()
}

Calling this function would return some concrete object that implements given trait. This however is currently not possible in Trait definitions. There is active development and hopefully in the future we will be able to use it. However now it's not the case. So following code will not compile!

// XXX: This doesn't currently work!
trait Foo {
    fn foo() -> impl Iterator<Item = ()>;
}

There is luckily a simple solution that wouldn't require boxing trait objects. You can define an associated type and require each implementator to specify a concrete type. You can also require that this type must implement the Iterator trait.

trait WordsFilter {
    type Iter: Iterator<Item = String>;
    
    fn starting_with(&self, chr: char) -> Self::Iter;
}

This would make each implementation specify one concrete type of returned iterator. If you would like to be able to return multiple different iterators for a single type you can use generics.

trait WordsFilter<I>
where
    I: Iterator<Item = String>
{
    fn starting_with(&self, chr: char) -> I;
}

Upvotes: 3

Related Questions