Impl TryInto as an argument in a function complains about the Error conversion

Here is what I am trying to do:

use std::convert::{TryFrom, TryInto};

fn main() -> Result<(), CommonError> {
    let input = String::from("this is a long string");
    //This works fine
    //note that it automatically converts the StringTooLongError -> CommonError
    Ok(println!("{:?}", MyType::try_from(input)? ))
    //that would have been better
    //Ok(println!("{:?}", convert(input)? ))
}

//but this fails: the trait `std::convert::From<<impl TryInto<MyType> as 
std::convert::TryInto<MyType>>::Error>` is not implemented for 
`StringTooSmallError`
//why? 
/*
fn convert(s: impl TryInto<MyType>) -> Result<MyType, 
StringTooSmallError> {
    Ok(s.try_into()?)
}
*/

#[derive(Debug)]
enum CommonError {
    StringError
}
impl From<StringTooSmallError> for CommonError {
    fn from(e: StringTooSmallError) -> Self {
        CommonError::StringError
    }
}


#[derive(Debug)]
struct MyType(String);

#[derive(Debug)]
struct StringTooSmallError;
/*impl std::fmt::Display for StringTooSmallError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "string is too small")
    }
}

impl std::error::Error for StringTooSmallError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        None
    }
}*/


impl TryFrom<String> for MyType {
    type Error = StringTooSmallError;
    
    fn try_from(s: String) -> Result<Self, StringTooSmallError> {
        let len = s.len();
        match len {
            0..=4 if len < 4 => Err(StringTooSmallError),
            _ => Ok(MyType(s[..=5].to_owned()))
        }
    }
}

I think my problem is self explained in the code. Here is in play: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=46d2dcdaabe0741a1a2b1b8ea8d9ad4c

I thought that having specified the TryFrom, TryInto should come for free, but apparently it doesn't. I guess it would work in the case of From/Into, but I would really like to achieve the same easiness with the TryFrom/TryInto traits. Is it possible or should I just abandon my effort ?

You didn't tell the compiler that your impl TryInto parameter should have StringTooSmallError as its error type. Without that constraint, the error type is allowed to be anything, and you can't make a StringTooSmallError out of anything.

So try changing the signature to:

fn convert(s: impl TryInto<MyType, Error=StringTooSmallError>) -> Result<MyType, StringTooSmallError>

If you want even more flexibility, you can specify that the error type is merely convertible to StringTooSmallError:

fn convert<T>(s: T) -> Result<MyType, StringTooSmallError>
    where T: TryInto<MyType>,
          StringTooSmallError: From<T::Error>
{
    Ok(s.try_into()?)
}

Or slightly more idiomatically:

fn convert<T>(s: T) -> Result<MyType, StringTooSmallError>
    where T: TryInto<MyType>,
          T::Error: Into<StringTooSmallError>,
{
    s.try_into().map_err(Into::into)
}

In Rust, how you use a generic function or type doesn't affect its behavior. The type system is sound from this point of view; it's nothing like you might be used to in the case of e.g. C++ templates. Generics in Rust are type checked once and only once, using only the constraints explicitly specified via trait bounds and associated type constraints.

Code in the body of a generic function won't pass type checking if it relies on some additional, unstated constraints and invariants, other than those described in the function signature. This holds even if you only ever call the function with types that do uphold these implicit constraints.

This is good because it makes sure that you can't possibly forget to state any constraint your implementation needs, and there won't be any unpleasant surprises – once your generic function type checks once, you are sure that it will compile and run correctly forever, with any set of type parameters that obey the required constraints.

Contrast this with C++ templates. In C++, library writers often find themselves in trouble because they have written their generic functions with some, but not all, constraints in mind, but they only find out later, when users of the library are unable to call the aforementioned generic functions with some types they were intended to be usable with, because the constraints not matching up is only detected once somebody actually calls (and thus instantiates) the generic function, and not at template checking time.

2 Likes

wow awesome reply! Many thanks for the whole explanation :smiley: