How to cause automatic deref when using from() of From <&str> with &String argument?

#1

Here is an example

#![warn(trivial_casts)]
const TEST_STR: &'static str="0123456789ABCDEFGH";

struct Digits(String);

impl Digits {
    fn from_string(v: String) -> Self {
        Self::from_str(&v)
    }
    
    fn from_str(v: &str) -> Self {
        let mut s=String::new();
        for c in v.chars() {
            match c {
                '0' ... '9' => s.push(c),
                _ => {},
            }
        }
        Digits(s)
    }
}

impl From<&str> for Digits {
    fn from(v: &str) -> Self {
        Self::from_str(v)
    }
}

fn get_vals(input: String) -> (Digits, Digits, Digits) {
    let val1=Digits::from_string(input);
    let val2=Digits::from_str(&input);
    let val3=Digits::from(&input);
    (val1, val2, val3)
}

fn main() {
    let val1=Digits::from_string(TEST_STR.to_string());
    let val2=Digits::from_str(TEST_STR);
    let val3=Digits::from(TEST_STR);
    let (val4, val5, val6)=get_vals(TEST_STR.to_string());
    println!("{}", val1.0);
    println!("{}", val2.0);
    println!("{}", val3.0);
    println!("{}", val4.0);
    println!("{}", val5.0);
    println!("{}", val6.0);
}

results in error:

the trait bound `Digits: std::convert::From
<&std::string::String>` is not satisfied

5 conversions out of 6 are fine. The problem, it seems, is in using from() of From trait, when it is implemented for Deref result only (that is &str), and not for the type of the identifier used (which is String in this case).

  1. Trying to specify the type to cause deref.
    Using
let val3=Digits::from(&input as &str);

removes this error, but a trivial-casts lint could get you now (if it is on, as in this example). Which is generally strange - for why didn’t it work automatically before, if the cast is THAT trivial. :slight_smile: Well, the lint gives some hint - this might require a temporary variable. Using something like

let val3=Digits::from({ let r:& str=&input; r});

is not so trivial and at the same time specific enough to evade both messages. But now it looks much less readable :frowning:

  1. Calling deref directly
    Adding use std::ops::Deref and changing &input to input.deref() also solves the case. But it still seems as unnecessary code complications in places where the compiler might already have the information it needs.

  2. Using String method as_str()
    Changing &input to input.as_str() is the shortest way so far. Which does implicit deref inside, that is - similar to solution No 1 with temporary variable really being a parameter &self of as_str() function. Though it is neat enough, it still leaves the question about auto-derefing open.

What might be the compiler original reasoning: “The Digits doesn’t implement From from String type, hmm, but the String derefs to &str, which the Digits implements From from… As there are no other derefs and no other From implementations in scope, then that must be the thing the programmer intended”. At least I thought so. Or did I miss something which ruins this reasoning?

0 Likes

#2

Coercions will never happen if Rust has to do any type inference, so unless you specify which From impl you want, coercions won’t happen.

0 Likes

#3

If you view this with compiler-colored glasses, this is what you see instead:

Deref coercion from &String to &str can only happen at a coercion site.

  • A function argument is a coercion site, but coercion won’t happen if the input types can be unified. When the argument type is just a generic T, no coercion happens because the compiler can say T=&String.
    Trait bounds play no role in unification.
  • An as cast is a coercion site (an “explicit coercion”)

I cannot explain why coercions are limited in this manner, but I know very little about the theory behind type inference and trait solving. At the end of the day, I’m comfortable enough with the workaround of just adding an impl for &String.

2 Likes

#4

Thank you. I solved it with .as_str() for now, for from(v: &str) is just the needed minimum.

As for the question “why?”
Methinks, it is like the simplest way for compiler to solve the problem with two variables (by variables here I mean mathematical ‘unknown values’, that is ‘unknown types’ here), by limiting it to one variable.
The compiler can seek a type for type inference, to choose appropriate generic (if the programmer did not specify one). Or it can seek a type for coercion (at coercion site, if the types mismatch).
But it won’t do both at the same time, when it knows neither the target type for deref (which is &str to be derefed from String) nor the source type for generic (which is also &str, to choose generic converting from &str to Digits) - it is about some intermediate unknown type (&str really), which is information the compiler lacks to process this.

So it is all about making compiler more smarter (and much more slower) by searching not the type, but the path through one or many types, using any available generics and coercions. And give error in case there is not a single path (if no path or two or more paths are available). Such compiler upgrade would bring several other complications, and could often result not in the conversion intended. So it better remain as it is. :slight_smile:

1 Like