Is it possible to fulfill this function signature?

Given the following external structs and traits which I do not own (simplified a bit):

Third-party code
pub struct ExternalContainer<T>(T);
impl<T> ExternalContainer<T> {
   pub fn from_trait(dummy: T, _et: impl ExternalStreamTrait<T> + 'static) -> Self {
      Self(dummy)
   }
}

pub trait ExternalMainTrait {
    fn create_container(&self) -> ExternalContainer<Data>;
}

pub trait ExternalStreamTrait<T> {
    type Item;
    fn bar(self: Box<Self>, s: BoxStream<T>) -> BoxStream<Self::Item>;
}

And given this code that I do control:

Code I own
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Data {
    Value,
}

//Can't be cloned
pub struct FieldA;
impl FieldA {
   pub fn a_stream<'a>(&'a self, _s: BoxStream<'a, String>,) -> BoxStream<'a, Data>
   {
      // Assume that this variable is initialized using self.some_field.some_operation
      let a = Data::Value;
      async move { a }.into_stream().boxed()
   }
}

//Can't be cloned
pub struct FieldB;
impl FieldB {
   pub fn another_stream_too(&self) -> BoxStream<String>
   {
      async move { "".to_string() }.into_stream().boxed()
   }
}

//Can't be cloned
pub struct ChildA {
   field_a: FieldA,
   field_b: FieldB,
}
impl ChildA {
    pub fn stream(&self) -> BoxStream<Data> {
        self.field_a
            .a_stream(self.field_b.another_stream_too())
            .boxed()
    }
}

//Can't be cloned
pub struct ChildB;


//Can't be cloned
pub struct Parent {
    pub child_a: ChildA,
    pub child_b: ChildB,
}
impl Parent {
    pub fn new(child_a: ChildA) -> Self {
        Self {
            child_a,
            child_b: ChildB {},
        }
    }
}

//Can't be cloned
pub struct ExternalStreamTraitImpl {
    pub inner: ChildA,
}
impl<T> ExternalStreamTrait<T> for ExternalStreamTraitImpl {
    type Item = Data;

    fn bar(self: Box<Self>, _s: BoxStream<T>) -> BoxStream<Self::Item> {
        self.inner.stream()
    }
}

pub struct Main {
    parent: Parent,
}

impl Main {
    fn new() -> Self {
        let parent = Parent::new(ChildA {
           field_a: FieldA,
           field_b: FieldB
        });

        Self { parent }
    }
}

impl ExternalMainTrait for Main {
    fn create_container(&self) -> ExternalContainer<Data> {
        ExternalContainer::from_trait(
           Data::Value,
           ExternalStreamTraitImpl {
                inner: self.parent.child_a,
            },
        )
    }
}

Is it possible to have it compile given the create_container method signature? The stream function from ChildA doesn't compile because the code has been stripped down and omitted field_a and field_b for demonstration purposes, but it is something like that.

I've tried many different variations but I've been unable to have it compile and at the same time maintain the code structure that I do control.

Playground demo

It would be helpful if you could provide the exact error message. Without that or a fully working example on the playground the best we can usually do is guess what the problem might be.

The only thing that really sticks out in a quick once-over is that you seem to be attempting to move child_a out of self.parent. If that's the case, Rust will reject the code because it would be invalid to transfer ownership and then later attempt to use self.parent.child_a.

Cloning child_a might be a solution if you do not expect the external container and your Parent to reference the same child_a struct. If you do need to reference the child from both places, your ExternalStreamTraitImpl could hold a reference to a ChildA type instead of holding ownership over one. Shared ownership is another option, and can be easier to deal with.

Again, this all just shooting in the dark. More info would be helpful.

You are totally right, let me fix that. I'll update my original post with a link to a full example in the playground, though it'll not compile because I don't know how.

I have updated my original post with a link to the playground. I am not sure if that is sufficient, but if not I can continue adding more realism to the demo. It's just that there are many parts to it and try to have it all in a small demo is difficult.

Thanks! That is exactly what was needed.

So there are two things:

  1. I was able to identify the issue with moving child_a out of self.inner, and using a reference-counting pointer worked for that.
  2. The other thing I wasn't able to identify was the ExternalStreamTrait::bar implementation trying to borrow from self.inner for longer than necessary. This is fixed with a few lifetime annotations.

I started with Rc to turn the move into a cheap clone:

use std::rc::Rc;

//Can't be cloned
pub struct Parent {
    pub child_a: Rc<ChildA>,
    pub child_b: ChildB,
}
impl Parent {
    pub fn new(child_a: ChildA) -> Self {
        Self {
            child_a: Rc::new(child_a),
            child_b: ChildB {},
        }
    }
}

//Can't be cloned
pub struct ExternalStreamTraitImpl {
    pub inner: Rc<ChildA>,
}
impl ExternalMainTrait for Main {
    fn create_container(&self) -> ExternalContainer<Data> {
        ExternalContainer::from_trait(
           Data::Value,
           ExternalStreamTraitImpl {
                inner: Rc::clone(&self.parent.child_a),
            },
        )
    }
}

Then I fixed the lifetime annotations on the boxed streams:

//Can't be cloned
pub struct FieldA;
impl FieldA {
    pub fn a_stream<'a>(&self, _s: BoxStream<'a, String>,) -> BoxStream<'a, Data> {
        // Assume that this variable is initialized using self.some_field.some_operation
        let a = Data::Value;
        async move { a }.into_stream().boxed()
    }
}

//Can't be cloned
pub struct FieldB;
impl FieldB {
    pub fn another_stream_too<'a>(&self) -> BoxStream<'a, String> {
        async move { "".to_string() }.into_stream().boxed()
    }
}

//Can't be cloned
pub struct ChildA {
   field_a: FieldA,
   field_b: FieldB,
}
impl ChildA {
    pub fn stream<'a>(&self) -> BoxStream<'a, Data> {
        self.field_a
            .a_stream(self.field_b.another_stream_too())
            .boxed()
    }
}

And here's the full thing on the playground.

1 Like

This is great. Thank you very much for your time. I'll try to adapt this to the original code and see if it works as clean as here because the full code has more thorns in it.

Yes, that is usually the expected case! The takeaway is that it's ok to use shared ownership if necessary (in this case I think it is...) and that the lifetime annotations can be finicky. The real trick to understanding the latter is to just think about how references relate to one another1; in the original code, the lifetime annotation claimed the input and output had an equivalent lifetime. I changed it so the lifetimes between input and output are unique.

1 Well, this has been the way I've become most comfortable with lifetimes. I don't know if this is common, or even "correct", but it works out for me in most cases.

It lives, it lives!! Thank you for your help!

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.