Upcasting a trait behind a smart pointer (Rc, etc)

Okay, so slipping in an extra Rc didn't work.

Also, I'm beginning to think my overall design is just too difficult to implement anyways and that I need to rethink the whole thing over.


What I had was something like this:

struct Context { /* stuff */ }

// Represents an expensive computation
trait Analyze {
    type Data: Data;

    fn analyze(&self, ctx: &Context) -> Self::Data;
}

// An intermediate form from which various types of reports may be produced
trait Data {
    // a variety of output functions...
}

There could be data dependencies between some of the Analyze objects, and so I wanted to allow one to take another (statically typed) during construction.

struct AnalysisA;
struct AnalysisB<'a>(&'a AnalysisA, OtherStuff);

...well, actually, some of the computations were expensive, so I wanted each one to only run once.

struct CachedAnalysis<A: Analysis + ?Sized> {
    cache: Rc<RefCell<Option<Rc<A::Data>>>>,
    analysis: Rc<A>,
}

struct AnalysisA;
struct AnalysisB(CachedAnalysis<AnalysisA>, OtherStuff);

But I also wanted to make the analyses composable. The idea was I could build up one "composite" Analysis from all the other analyses, and have it produce some grand Data value whose output functions synthesize the results from each of the individual Datas.

But in order to have a homogenous collection of Analyses, I needed to erase the types of not just the analyses themselves, but also their associated types.

pub type ErasedAnalysis = CachedAnalysis<Analyze<Data=Data>>;
pub struct CompositeAnalysis(Vec<ErasedAnalysis>);
pub struct CompositeData(Vec<Rc<Data>>);

And here's the pincher: Of course, we would want the type-erased analyses to be tied to the same caches as the originals, right? But we can't just turn an Rc<RefCell<Option<Rc<A>>>> into an Rc<RefCell<Option<Rc<Data>>>> without building a new RefCell (because the size of its contents must change to accommodate the vtable).

Therefore, the Data in CachedAnalysis must always have its type erased, even when its actual type is already known. And because AnalysisB doesn't have much use for storing AnalysisA if it can't see that analysis' data, this is why downcasting was necessary.

pub struct ErasedData(Rc<Data>);
pub struct CachedAnalysis<A: ?Sized> {
    cache: Rc<RefCell<Option<ErasedData>>>  // <---- !!
    analysis: Rc<A>,
}

pub type ErasedAnalysis = CachedAnalysis<Analyze<Data=ErasedData>>;

pub struct CompositeAnalysis(Vec<ErasedAnalysis>);
pub struct CompositeData(Vec<ErasedData>);

impl ErasedData {
    pub fn downcast<D: Data>(&self) -> Option<Rc<D>>;
}

impl<A: Analysis> Analyze for CachedData

impl<A: Analysis> CachedAnalysis<A> {
    pub fn get_or_compute(&self, ctx: &Context) -> Rc<A::Data>
    { /* compute if necessary, then use `downcast()` */ }

    pub fn erase_type(self) -> ErasedAnalysis
    { /* wrap `analysis` in a newtype whose Data is ErasedData */ }
}

Anyways, after getting over the Rc speedbump, I had to fix several bugs where I didn't deref enough times and had the wrong type get upcast into a trait object, and even now there's still one bug left that I'm too tired to figure out. You can see what I have here, but I can tell it's just going to be miserable if I continue this way.

For now I'm trying an alternate design that decouples computations from outputs. Computation is "monad-like" and the outputs are "monoid-like", so the problems are probably best solved in orthogonal ways. For now I'm making a trait with combinators for self-caching computations, and I imagine the outputs will be easily managed somehow.


Edit: ...wait a minute, what's the trait for then? Why am I composing objects that compute something and cache it, when I could instead just call regular functions that compute the thing right then and there, and use those values as regular arguments to other functions?! I think I need a time out...