Search code examples
genericsrustownership

Avoiding explicit type annotation for a struct constructor, where the struct contains a generic field


I am trying to create a struct that contains something that implements a trait which can be either an owned type or a ref-counted pointer such as Arc, Rc, etc. So far I have implemented the following using the Borrow trait, however, the compiler is forcing me to annotate the types of the instances returned by the new function, which is not ideal for the users of my crate.

How can I modify the following code, so that the user does not need to annotate the types of the instances returned by the new function?

struct Foo<B, T> {
    borrow: B,
    _marker: std::marker::PhantomData<T>
}

impl<B, T> Foo<B, T> {
    fn new(borrow: B) -> Self {
        Self {
            borrow,
            _marker: Default::default()
        }
    }
}

impl <B, T> Foo<B, T> where
    B: std::borrow::Borrow<T>,
    T: std::fmt::Display {
    fn print(&self) {
        println!("{}", self.borrow.borrow())
    }
}

fn main() {
    // owned string (remove type annotation for error)
    let f: Foo<_, String> =
        Foo::new(String::from("hello"));
    f.print();

    // string slice (remove type annotation for error)
    let f: Foo<_, &str> =
        Foo::new("hello");
    f.print();

    // string in Arc (remove type annotation for error)
    let f: Foo<_, std::sync::Arc<String>> =
        Foo::new(std::sync::Arc::new(String::from("hello")));
    f.print();
}


Solution

  • The problem with your code is that a type, your B can implement Borrow<T> for many different T types, so if you don't specify the T somehow, the call will be ambiguous. The same is true for AsRef.

    But it is not for Deref. That can only be implemented once per type, and it is already implemented for any std smart pointer.

    The code is straightforward, because you can constrain the Deref::Target type:

    use std::ops::Deref;
    
    impl<B> Foo<B>
    where
        B: Deref,
        <B as Deref>::Target: std::fmt::Display,
    {
        fn print(&self) {
            println!("{}", self.obj.deref());
        }
    }
    

    This has the drawback that will only work for smart-pointer-like types. That is if for example B = i32 it will not work because a plain type such as i32 doesn't implement Deref:

        let f: Foo<_> = Foo::new(42);
        f.print();
    
    > error[E0599]: the method `print` exists for struct `Foo<{integer}>`, but its > trait bounds were not satisfied
    > note: trait bound `{integer}: Deref` was not satisfied
    > where
    >      B: Deref,
    >         ^^^^^ unsatisfied trait bound introduced here
    

    If that is ok for you, you can stop reading here.

    If that is not ok, I think the best option is to forget the AsRef/Deref/Borrow thing and just require that B implements either the trait you want, or a facade for that trait.

    The details would vary depending on exactly what you want to implement. If your trait is actually Display and that is not a placeholder, then you don't actually need anything special, because Display is implemented for Box<T>, Rc<T>, Arc<T>, etc. when T: Display. So you just write:

    impl<B> Foo<B>
    where B: std::fmt::Display,
    {
        fn print(&self) {
            println!("{}", self.borrow)
        }
    }
    

    But I'm guessing that your actual trait is not Display. Again the details vary depending on whether you own that trait or not. Imagine this trait:

    trait Printable {
        fn print(&self);
    }
    impl Printable for str {
        fn print(&self) { println!("s:{}", self); }
    }
    impl Printable for i32 {
        fn print(&self) { println!("i:{}", self); }
    }
    

    If it is yours you can write the following code directly, but if it is third-party you will need a facade trait:

    trait AsPrintable {
        type Target: Printable + ?Sized;
        fn as_printable(&self) -> &Self::Target;
    }
    

    And your Foo would be:

    impl<B> Foo<B>
    where
        B: AsPrintable,
    {
        fn print(&self) {
            self.obj.as_printable().print();
        }
    }
    

    Then you can implement the trait for any types you want. The small inconvenient here is that you would like two write two blanket implementations:

    impl<T: Printable> AsPrintable for T {
        type Target = T;
        fn as_printable(&self) -> &T { self }
    }
    impl<T: Deref> AsPrintable for T
        where <T as Deref>::Target: AsPrintable
    {
        type Target = <<T as Deref>::Target as AsPrintable>::Target;
        fn as_printable(&self) -> &Self::Target { self.deref().as_display() }
    }
    

    But only one or the other is allowed, Rustc will not allow both because of ambiguity. If needed, this can be fixed with a few manual implementations for the needed wrappers. Or a new-type to replace the first blanket impl, such as this one:

    struct MakePrintable<T>(T);
    
    impl<T: Printable> AsPrintable for MakePrintable<T> {
        type Target = T;
        fn as_printable(&self) -> &T { &self.0 }
    }
    

    Here is a playground with the whole example.