Layered Designs, Types, and Monomorphism

Context

I'm building a layered application, and I would like to use generic types to be able to test each layer independently of the others.
Very generally, (and incorrectly, as lifetimes are omitted) this would look like

trait Layer1 {}
struct RealLayer2<L1: Layer1> {
    layer1: &L1,
}

#[cfg(test)]
mod test {
    struct FakeLayer1 {}
    impl Layer1 for FakeLayer1 {} 
    #[test]
    test_foo() {
        let mut l2 = RealLayer2<FakeLayer1>{}
    }
}

That is, RealLayer2 can be used monomorphically with FakeLayer1 to do some testing, and then again monomorphically with RealLayer1 in production code.

Concretely

The first layer is a content-addressible storage layer, parameterized on a type to be stored, T. I have (with function bodies omitted, since the question is about the method types):

type Hash = String;
pub trait CAS<T> {
    fn store(&self, value: &T) -> String;
    fn retrieve(&self, hash: &Hash) -> Option<T>;
}

pub struct Storage<T: Encodable + Decodable> {}   
impl<T: Encodable + Decodable> CAS<T> for Storage<T> {
    fn store(&self, value: &T) -> Hash {}
    fn retrieve(&self, hash: &Hash) -> Option<T> {}
}

pub struct FakeStorage<T: Encodable + Decodable> {}   
impl<T: Encodable + Decodable> CAS<T> for FakeStorage<T> {
    fn store(&self, value: &T) -> Hash {}
    fn retrieve(&self, hash: &Hash) -> Option<T> {}
}

The second layer is a git-like filesystem. It requires something that implements CAS<Object>, where Object is a type specific to the filesystem layer.

pub enum Object {}
pub trait FS<'a, C>
    where C: 'a + CAS<Object>
{
    fn root_commit(&self) -> Commit<'a, C>; 
    fn get_commit(&self, hash: Hash) -> Result<Commit<'a, C>, String>;
}

pub struct FileSystem<'a, C: 'a + CAS<Object>> {
    storage: &'a C,
}   
    
impl<'a, C> FS<'a, C> for FileSystem<'a, C> 
    where C: 'a + CAS<Object>
{
    fn root_commit(&self) -> Commit<'a, C> {}
    fn get_commit(&self, hash: Hash) -> Result<Commit<'a, C>, String> {}
}

The intent with the lifetime 'a is that everything has a lifetime shorter than the content-addressible storage layer. That layer uses interior mutability to allow liberal sharing of immutable references, with the result that anything needing access to the storage layer can find a pointer to it easily, allowing lazy loading.

The Problem

The FS trait references type Commit<'a, C>, where that type is a part of the definition of the "real" filesystem. That will preclude using a FakeCommit struct there.

Help?

What is the most idiomatic way to approach this situation?

I am told associated types will not work, because higher-order types are not supported.

One idea is to define traits for the secondary types like Commit. But this means that the FS methods return Trait objects, leading to some lifetime issues (who owns those trait objects?). Moreover, it means that I will use dynamic dispatch in production, even though there will only be one Commit type in use in production.

Perhaps the only viable option is to define Commit such that it does not contain any real implementation, and can thus be shared between the real and fake FS implementations.

Hm, I think a combination of both works.

pub trait FS
{
    type Commit: Committish;

    fn root_commit(&self) -> Self::Commit; 
    fn get_commit(&self, hash: Hash) -> Result<Commit, String>;
}

impl<'a, C: 'a + CAS<Object>> FS for for FileSystem<'a, C> {
    type Commit: Commit<'a, C>;

     fn root_commit(&self) -> Self::C { ... };
}

Can you post a minimal, not-working example to play.rust-lang.org? I'd like to play around

2 Likes

This is an adaptation of the question. Here is an updated version based on your answer. I'm not sure what you meant by "Committish", but the second example actually compiles, so it may be the right solution!

Indeed, this seemed to work: