Using Into as a return type

I am trying to make the return type of a function more
general. Currently, I've had to give it a very specific type which
means I am making some unnecessary type conversions.

While playing around I think maybe the solution is to use Into; I
could have the function return an Into type which would allow me to
convert it but only if necessary.

So I have tried this:

pub fn f() -> String {
    "hello".to_string()
}

pub fn g() -> impl Into<String> {
    "hello".to_string()
}

pub fn h<S:Into<String>>() -> S {
    "hello".to_string()
}

In the first function, we have a concrete type. In the second, we use
impl and this works and compiles. In the last case, I return an Into
type. This fails, though because the return statement returns a
String. But String implements Into<String> I though, because all
things implement Into<Themselves>.

What have I misunderstood?

1 Like

Generic argument is decided by the caller, not the implementor. You can treat it as a function argument but in the type level. If the third function compiles, what type can the expression below have?

h::<&'static str>()

It manually set the type S as &'static str using the turbofish syntax. Now the S is the &'static str which satisfy the Into<String> generic bound. And the S is the return type of the function. Does its body returns &'static str now?

4 Likes

In other words: to_string() returns a String, not any type S that implements Into<String>. This means that you could convert an instance of S to a String, if you had one. It does NOT mean you can convert a String into an instance of S.

This is more formally expressed by the tools of first-order logic, as follows.

  • impl Trait in return position is called an "existential type". The signature fn g() -> impl Into<String> means: "There exists some concrete type that I choose to return. I don't tell you what exactly it is, just that it is guaranteed to implement Into<String>". A return-position impl Trait thus gives guarantees to the caller.

  • On the other hand, generic <Arguments> are called "universally-quantified". A declaration like fn h<S: Into<String>>() -> S means: "You are allowed to choose any type S as long as it implements Into<String>. Therefore, a generic type parameter gives guarantees to the callee.

    This particular one is a rather odd bound for a return type, though, because it doesn't say anything about how such a type can be instantiated, therefore the only correct implementation of such a function is a trivial, diverging one, e.g. panic!() or an infinite loop {}.

    What you could reasonably write is this:

    fn h<S: From<String>>() -> S {
        S::from("hello".to_string())
    }
    

    where the S: From<String> trait bound expresses the fact that you can create an instance of S from a String. It still lets the caller choose the type, though.


Note that impl Trait in argument position is more or less equivalent with a generic type parameter, therefore it is universal in that context, and not existential.

3 Likes

Thank you for the detailed explanation. So the best that I have come up with to achieve something similar, is to use an enum and then implement Info over that.

In this case, the enum has three types and I only have to convert from something else to a String in one case. But if I wanted to get the number out I could do so.

Does this seem a reasonable way to address the problem?

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.