genericsrustpolymorphismtraitsparametric-polymorphism

How do implement a struct that takes a trait MyTrait<A>?


I've defined a trait as follows:

trait Readable<E> {
    fn read_u8(&mut self) -> Result<u8, E>;
    fn read_u16be(&mut self) -> Result<u16, E>;
}

The idea is to implement it with different backends returning different error types. I tried to use it in a function:

fn f<E, R: Readable<E>>(r: &mut R) -> Result<u8, E> {
    r.read_u8()
}

This compiles. I tried the same in an impl:

struct FileFormat<R> {
    r: R,
}

impl<E, R: Readable<E>> FileFormat<R> {
    fn f(&mut self) -> Result<u8, E> {
        self.r.read_u8()
    }
}

Playground

This fails with:

14 | impl<E, R: Readable<E>> FileFormat<R> {
   |      ^ unconstrained type parameter

The compiler suggests rustc --explain E0207 but I'm afraid I haven't been able to understand the answer contained within if it's there.

Why does the former compile while the latter doesn't? Why is E in this case is unconstrained? How to resolve this so that the implementation will be able to take any Readable?


Solution

  • Let's imagine for a moment that it did compile. Here's a commented example which shows it would allow us to write broken code which would be impossible for the compiler to type check:

    trait Readable<E> {
        fn read_u8(&mut self) -> Result<u8, E>;
        fn read_u16be(&mut self) -> Result<u16, E>;
    }
    
    fn f<E, R: Readable<E>>(r: &mut R) -> Result<u8, E> {
        r.read_u8()
    }
    
    struct SomeError;
    struct SomeOtherError;
    struct SomeReadable;
    
    impl Readable<SomeError> for SomeReadable {
        fn read_u8(&mut self) -> Result<u8, SomeError> {
            todo!()
        }
        fn read_u16be(&mut self) -> Result<u16, SomeError> {
            todo!()
        }
    }
    
    impl Readable<SomeOtherError> for SomeReadable {
        fn read_u8(&mut self) -> Result<u8, SomeOtherError> {
            todo!()
        }
        fn read_u16be(&mut self) -> Result<u16, SomeOtherError> {
            todo!()
        }
    }
    
    struct FileFormat<R> {
        r: R,
    }
    
    // let's pretend that this does compile
    impl<E, R: Readable<E>> FileFormat<R> {
        fn f(&mut self) -> Result<u8, E> {
            self.r.read_u8()
        }
    }
    
    // it will now allow us to write this code
    // which is impossible to type check so
    // it's obviously broken
    fn example(mut fr: FileFormat<SomeReadable>) -> Result<u8, ???> {
        // um, does this return Result<u8, SomeError>
        // or does it return Result<u8, SomeOtherError>???
        // it's impossible to know!
        fr.f()
    }
    

    playground

    The error type needs to be present somewhere within the FileFormat type. The fix is as simple as adding a PhantomData member to FileFormat so you can "pin down" a specific error type:

    use core::marker::PhantomData;
    trait Readable<E> {
        fn read_u8(&mut self) -> Result<u8, E>;
        fn read_u16be(&mut self) -> Result<u16, E>;
    }
    
    fn f<E, R: Readable<E>>(r: &mut R) -> Result<u8, E> {
        r.read_u8()
    }
    
    struct SomeError;
    struct SomeOtherError;
    struct SomeReadable;
    
    impl Readable<SomeError> for SomeReadable {
        fn read_u8(&mut self) -> Result<u8, SomeError> {
            todo!()
        }
        fn read_u16be(&mut self) -> Result<u16, SomeError> {
            todo!()
        }
    }
    
    impl Readable<SomeOtherError> for SomeReadable {
        fn read_u8(&mut self) -> Result<u8, SomeOtherError> {
            todo!()
        }
        fn read_u16be(&mut self) -> Result<u16, SomeOtherError> {
            todo!()
        }
    }
    
    struct FileFormat<R, E> {
        r: R,
        e: PhantomData<E>,
    }
    
    // now compiles!
    impl<E, R: Readable<E>> FileFormat<R, E> {
        fn f(&mut self) -> Result<u8, E> {
            self.r.read_u8()
        }
    }
    
    // now works!
    fn example(mut fr: FileFormat<SomeReadable, SomeError>) -> Result<u8, SomeError> {
        fr.f()
    }
    
    // now also works!
    fn other_example(mut fr: FileFormat<SomeReadable, SomeOtherError>) -> Result<u8, SomeOtherError> {
        fr.f()
    }
    

    playground

    The standalone generic function works because we specific the Error type when we call the function:

    trait Readable<E> {
        fn read_u8(&mut self) -> Result<u8, E>;
        fn read_u16be(&mut self) -> Result<u16, E>;
    }
    
    struct SomeError;
    struct SomeOtherError;
    struct SomeReadable;
    
    impl Readable<SomeError> for SomeReadable {
        fn read_u8(&mut self) -> Result<u8, SomeError> {
            todo!()
        }
        fn read_u16be(&mut self) -> Result<u16, SomeError> {
            todo!()
        }
    }
    
    impl Readable<SomeOtherError> for SomeReadable {
        fn read_u8(&mut self) -> Result<u8, SomeOtherError> {
            todo!()
        }
        fn read_u16be(&mut self) -> Result<u16, SomeOtherError> {
            todo!()
        }
    }
    
    fn f<E, R: Readable<E>>(r: &mut R) -> Result<u8, E> {
        r.read_u8()
    }
    
    fn example() {
        let mut readable: SomeReadable = SomeReadable;
        // error type clarified to be SomeError here
        f::<SomeError, _>(&mut readable);
    
        let mut readable: SomeReadable = SomeReadable;
        // error type clarified to be SomeOtherError here
        f::<SomeOtherError, _>(&mut readable);
    }
    

    playground

    It really all just comes down to making your types visible to the compiler.