Type annotations needed on method call problem - find a way to define traits such that type annotations are not needed

Hello everyone!

I have a situation/code where i am not being able to define types such that trait method could be called without annotations. Clean code without comments in playground.

Have been wrestling with this for hours, but can not find the syntax/structures of how to describe this properly. I think i understand the problem, why explicit annotations are required. The code without comments is short, but i want to describe a problem context a little bit.

fn main() {
    let buf = Buffer {};
    let grant: () = buf.grant();
    //          ^----------This is where the problem starts, returned grant
    //                     has lost it's providers type. If this is fixed, i
    //                     think annotations would not be necessary.
    let s = "X".to_string();

    s.do_something(&grant);
    // ^---- This does not work, but i want to be able to use this short and
    // nice syntax.

    <String as DoWork<Buffer>>::do_something(&s, &grant);
    // ^---- This does what is intended, except that i would like to avoid
    // using this long syntax.
}


// =============================
// First part:
// =============================

// The goal for this is to define a trait that allows to influence the structure
// that produced the Grant. Let's say a Vec provides a Grant to mutably modify
// it's contents. Generally it's like wraping `&mut self.vec[..]` and then
// allowing to pass this grant to some other function calls defined by other
// traits.
// This provided grant should follow usual borrowing rules, if possible; that is
// if one &mut grant is provided, then another can not be taken (this is not
// mandatory though, structure is able to handle it internally).

trait PhantomGrant {
    type Grant: BufferGrant;

    fn grant(&self) -> Self::Grant;
}


// So let's say buffer just wraps Vec<u8> and returns some other type that
// wraps &[u8] and provides some more nice methods defined by BufferGrant trait.
struct Buffer {}
impl PhantomGrant for Buffer {
    type Grant = ();
    //            ^------ Here we have to use our custom type, it can have a
    //                    generic or whatever as long as it provides a compiler
    //                    with type information. Used tuple as an example for
    //                    shorter code.

    fn grant(&self) {}
}


// =============================
// Second part.
// =============================


// This is this other trait, that takes Grants as arguments, let's say it uses
// provided grant contents as &[u8] and prints if they equal.
trait DoWork<T> {
    type Grant: BufferGrant;

    fn do_something(&self, grant: &Self::Grant);
}


impl<T: PhantomGrant> DoWork<T> for String {
    type Grant = <T as PhantomGrant>::Grant;

    fn do_something(&self, _g: &Self::Grant) {
        // So here a string would compare it's contents with provided argument,
        // and print something if they equal.
        // In real requirements, &mut is used for concatenation, etc., but i
        // think it is not relevant for now.
    }
}


// These are grants provided methos, let's say it would allow to read vec
// contents through &[u8].
trait BufferGrant {}
impl BufferGrant for () {}

Compilation error:

   Compiling playground v0.0.1 (/playground)
error[E0283]: type annotations needed
  --> src/main.rs:6:7
   |
6  |     s.do_something(&grant);
   |       ^^^^^^^^^^^^
   |
   = note: cannot satisfy `_: PhantomGrant`
   = help: the trait `PhantomGrant` is implemented for `Buffer`
note: required for `String` to implement `DoWork<_>`
  --> src/main.rs:34:23
   |
34 | impl<T: PhantomGrant> DoWork<T> for String {
   |         ------------  ^^^^^^^^^     ^^^^^^
   |         |
   |         unsatisfied trait bound introduced here
help: try using a fully qualified path to specify the expected types
   |
6  -     s.do_something(&grant);
6  +     <String as DoWork<T>>::do_something(&s, &grant);
   |

For more information about this error, try `rustc --explain E0283`.

My questions are - is it possible to use short syntax and how to do it? Or at least get an affirmation that this currently is not possible so i accept this as it is and move on with the task. I am hopeful that maybe there is some clever wrapping or something that could allow to have a clean syntax to library function end users.

Thank you!

for traits with generic arguments, if there's exactly one non-generic impl block for a type, then it will be used for type inference, since there's no ambiguity. otherwise, the inference fails due to ambiguous implementation, thus you need annotations.

if you change the impl block into this, (and this is the only impl DoWork for String {}), then it should compile:

impl DoWork<Buffer> for String {
    // or equivalently: type Grant = ();
    type Grant = <Buffer as PhantomGrant>::Grant;
    fn do_something(&self, _g: &Self::Grant) {
        //...
    }
}

for type inference, associated types and generic type parameters in a trait behaves very differently.

While this compiles it looses the generic T: PhantomGrant and works only for single implementation. If i define Buffer2 i get back to the start, same problem as it was at the beginning - annotations needed. Playground example2.

The whole point for this generic is to allow to have different buffer implementations that provide a Grant through which contents can be accessed/modified. If each implementation would have to impl DoWork for String, etc. it would be okay, but what i am not okay with is explicit annotation requirement.

In example2, when 2 buffer implementations are provided in separate sub modules and only one is used, compiler still requires explicit annotations. I will check if separating them in different crates could help with this.

fn main() {
    use buffer::Buffer;
    let buf = Buffer {};
    let grant = buf.grant();
    let s = "X".to_string();

    s.do_something(&grant);
    <String as DoWork<Buffer>>::do_something(&s, &grant);
}



pub trait PhantomGrant {
    type Grant: BufferGrant;

    fn grant(&self) -> Self::Grant;
}


pub trait DoWork<T> {
    type Grant: BufferGrant;

    fn do_something(&self, buf: &Self::Grant);
}


pub trait BufferGrant {}



mod buffer {
    use super::{
        PhantomGrant,
        DoWork,
        BufferGrant,
    };

    pub struct Buffer {}
    impl PhantomGrant for Buffer {
        type Grant = Grant1;

        fn grant(&self) -> Grant1 {
            Grant1 {}
        }
    }


    impl DoWork<Buffer> for String {
        type Grant = <Buffer as PhantomGrant>::Grant;

        fn do_something(&self, _g: &Self::Grant) {}
    }

    pub struct Grant1 {}
    impl BufferGrant for Grant1 {}
}



pub mod buffer2 {
    use super::{
        PhantomGrant,
        DoWork,
        BufferGrant,
    };

    pub struct Buffer2 {}
    impl PhantomGrant for Buffer2 {
        type Grant = Grant2;

        fn grant(&self) -> Grant2 {
            Grant2 {}
        }
    }



    impl DoWork<Buffer2> for String {
        type Grant = <Buffer2 as PhantomGrant>::Grant;

        fn do_something(&self, _g: &Self::Grant) {}
    }


    pub struct Grant2 {}
    impl BufferGrant for Grant2 {}
}

that's the point. if there can be multiple implementations, which type would the type inference resolve to? your design is inherently ambiguous to begin with, that why you need annotation.

the problem with associated type is, they CANNOT be used to infer their implementing type.

put it in another way, from this code:

s.to_something(&grant)

the type of grant is known to be (), or, we can say the compiler knows <T as PhantomGrant>::Grant is (), but that's all it knows, it CANNOT deduce the type of T from that information!

1 Like

the fundamental issue in this design is the DoWork trait, where the trait is parameterized with one type T: PhantomGrant, but the methods do NOT depends on the generic type parameter, instead it depends on another type Grant: BufferGrant.

if the all the methods of DoWork only depends on the Grant type, then you should make DoWork parameterized over Grant, instead of using an associated type:

trait DoWork<T: BufferGrant> {
    fn do_something(&self, grant: &T);
}
impl<T: BufferGrant> DoWork<T> for String {
    fn do_something(&self, grant: &T) {
        //...
    }
}

this doesn't have the ambiguity, but you lose the ability to have different implementations for the same Grant type.

so the trade-off:

  • multiple implementations for the same Grant type, but ambiguous to infer, needs annotation;
  • no ambiguity, no annoation is needed, but different Buffer with same Grant type must share the same DoWork implementation;
1 Like

After careful consideration i see that i can take this trade-off and it allows to call methods without explicit annotations. Thank you @nerditation, it took me a few times to reread your two messages above till it sank in though. :slightly_smiling_face:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.