Skip to content

Instantly share code, notes, and snippets.

@djmitche
Last active February 28, 2017 04:02
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save djmitche/c1b24f41614b44fd3834b0d9aeb62b6f to your computer and use it in GitHub Desktop.
Save djmitche/c1b24f41614b44fd3834b0d9aeb62b6f to your computer and use it in GitHub Desktop.
Rust design question

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:

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

pub struct Storage<T: Encodable + Decodable> {
    // ...
}   

impl<T: Encodable + Decodable> Storage<T> {
    /// Create a new, empty storage pool.
    pub fn new() -> Storage<T> {
        // ...
    }
}

impl<T: Encodable + Decodable> CAS<T> for Storage<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 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> FileSystem<'a, C>
    where C: 'a + CAS<Object>
{
    pub fn new(storage: &'a C) -> FileSystem<'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.

The Problem

I won't bore you with the definitions of Commit<'a, C> and its friends. The issue is manifest just in this much exposition: the FS trait references concrete type Commit<'a, C>, which will preclude using a FakeCommit struct there.

The lifetime annotations are also getting a bit laborious, but maybe that's a price I have to pay.

Help?

What is the most idiomatic way to approach this situation?

The best idea I can think of 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment