Being generic over a container type but not its content

I have a feeling that this is either trivial or impossible.

I want to create a type that shall be generic over some container type but not over their content. Imagine being able to choose between Arc or Rc for the fields but the public interface has no knowledge about the inner types.

My first attempt using generics:

struct BlueContainer<T>(T);
struct RedContainer<T>(T);

trait GetInner<T> { fn get(&self) -> T; }

impl<T> GetInner<T> for BlueContainer<T> {
    fn get(&self) -> T { self.0 }
}

impl<T> GetInner<T> for RedContainer<T> {
    fn get(&self) -> T { self.0 }
}

// Is there a way to express that `Variable` shall not be bound to a certain type
struct Carrier<C<Variable>: GetInner<Variable>> {
    colorblind_string_container: C<String>,
    colorblind_int_container: C<i32>,
    red_string_container: RedContainer<String>,
    blue_int_container: BlueContainer<i32>,
}

My second attempt using associated types

struct BlueContainer<T>(T);

struct RedContainer<T>(T);

trait GetInner {
    type Inner;
    fn get(&self) -> &Self::Inner;
}

impl<T> GetInner for BlueContainer<T> {
    type Inner = T;
    fn get(&self) -> &Self::Inner { &self.0 }
}

impl<T> GetInner for RedContainer<T> {
    type Inner = T;
    fn get(&self) -> &Self::Inner { &self.0}
}

struct Carrier<C> where C: GetInner {
    colorblind_int_container: C, // should be something like `C::<i32>`
    colorblind_string_container: C, // should be something like `C::<String>`
}

fn main() {
    let carrier = Carrier {
        colorblind_int_container: BlueContainer(123_i32),
        // fails because BlueContainer has been used as i32 already
        colorblind_string_container: BlueContainer("asdf".to_owned()),
    };
    
    println!("{}", carrier.colorblind_int_container.get());
    println!("{}", carrier.colorblind_string_container.get());
}
1 Like

In the long term, Generic Associated Types (GATs) will provide a simple solution to this problem.

For now, you may need to use slightly more complicated methods. For example, the archery crate provides traits and types for abstracting over Arc<T> and Rc<T>.

4 Likes

Technically you are requiring that RedContainer (or RedContainer<_> or for<T> RedContainer<T>) be a usable entity within a meta-programming approach on its own.

  • When using generics, this means that it should be a type. In Rust, this is not the case, but you can make your own type:

    /// Container of a fixed `Elem` type (hence it being an assoc type instead of a generic type parameter)
    trait Get {
        type Elem;
        fn get (self: &'_ Self) -> &'_ Self::Elem
        ;
    }
    
    // Example
    struct RedContainer<Elem>(Elem);
    impl<Elem> Get for RedContainer<Elem> {
        type Elem = Elem;
    
        fn get (self: &'_ Self) -> &'_ Elem
        {
            &self.0
        }
    }
    
    /// Trait for a "higher-order type": expresses that `Self`
    /// has a "`<Elem = _> hole"
    /// The pseudo code "`Self<Elem>`" can be expressed with this
    /// helper trait as `<Self as ContainerOf<Elem>>::T`
    trait ContainerOf<Elem> {
        /// The "return" type of this type-level function:
        /// a concrete container type
        type T : Get<Elem = Elem>;
    }
    
    // Example
    enum RedContainer_ {}
    impl<Elem> ContainerOf<Elem> for RedContainer_ {
        type T = RedContainer<Elem>;
    }
    

    This way, you can write:

    struct Carrier<C : ContainerOf<String> + ContainerOf<i32>> {
        colorblind_string_container: <C as ContainerOf<String>>::T,
        colorblind_int_container: <C as ContainerOf<i32>>::T,
        red_string_container: RedContainer<String>,
        blue_int_container: BlueContainer<i32>,
    }
    

    and use it as Carrier<RedContainer_> or Carrier<BlueContainer_>.

  • when using macros as the meta-programming approach, it turns out that the "identifier" RedContainer can directly be fed the <T> type parameter.

    So, for instance, one possible macro-based API would be:

    macro_rules! mk_Carriers {(
        $( $Container:ident ),* $(,)?
    ) => (
        #[allow(nonstandard_style)]
        pub mod Carrier {
          $(
            pub struct $Container {
                colorblind_string_container: super::$Container<String>,
                colorblind_int_container: super::$Container<i32>,
                // etc.
            }
          )*
        }
    )}
    

    so that one can call mk_Carriers! { RedContainer, BlueContainer } and then use the types Carrier::RedContainer, or Carrier::BlueContainer.

    Not the prettiest thing, but it's an interesting alternative to higher-level generics :slight_smile:

2 Likes

Thanks a lot for your help. I understood most of your solution but I'll have to play with that for a while to make sure I understand the implications for the rest of my project design.

@mbrubeck So the short answer is "It's trivial if it were possible" :grin: thanks for the links - reading myself into the matter right now.

Specifically this sections seems to cover my use case: 1598-generic_associated_types - The Rust RFC Book