trait Num {
fn from_i32(n: i32) -> Self;
}
impl Num for f64 {
fn from_i32(n: i32) -> f64 { n as f64 }
}
// These 4 are all equivalent in this case.
let _: f64 = Num::from_i32(42);
let _: f64 = <_ as Num>::from_i32(42);
let _: f64 = <f64 as Num>::from_i32(42);
let _: f64 = f64::from_i32(42);
which i find quite helpful. However, the syntax for the first three let-statements is unintuitive to me.
Of course, I can just learn the fourth one; but for the other ones, it looks like magic that the compiler ends up using the type annotation to link it to the Self (of that's what seems to me.)
I'm sure there is some piece of knowledge I'm missing or not connecting to make it seem less magical and more attuned to the rest of the language patterns (at least basic ones I learnt.)
Edit: actually the 2 last statements are fine, because <f64 as X> is desambiguation and makes -some- sense to me.
In the 3rd, the <f64 as Num> specifies Self explicitly as f64, so it is no different than the 4th where it is also explicit (EDIT: as pointed out by @jofas, the 4th is ambiguous if there is more than one from_i32 impl).
In the 1st and 2nd, the f64 is specified on the left hand side using _:f64, so it can be inferred to match the return type on the right handle side.
The second and third examples are called fully qualified syntax, which is the only way to truly disambiguate between implementations. You need to know this syntax in cases where there are multiple from_i32 functions associated with a type like f64 in scope, for example:
trait Num {
fn from_i32(n: i32) -> Self;
}
impl Num for f64 {
fn from_i32(n: i32) -> f64 { n as f64 }
}
trait Num2 {
fn from_i32(n: i32) -> Self;
}
impl Num2 for f64 {
fn from_i32(n: i32) -> f64 { n as f64 }
}
fn main() {
let _: f64 = Num::from_i32(42);
let _: f64 = <_ as Num>::from_i32(42);
let _: f64 = <f64 as Num>::from_i32(42);
// Now this fails, because there are two `from_i32` functions defined for `f64`
let _: f64 = f64::from_i32(42);
}
yes, i think i edited while you were writing, the desambiguation i am familiar with, just the syntax is somewhat odd but will need to accept it, and the <_ as..> i guess it means inference, i don't think that's intuitive either.
You can think of all of them as sugar for the qualified path expression -- the path that starts with either <Ty>:: or <Ty as Trait>::. <Ty>:: will look for items in the implementations of Ty, both inherent and in traits. <Ty as Trait>:: will restrict the search to the specific trait.
let _: f64 = Num::from_i32(42);
// => <_ as Num>::from_i32(42);
let _: f64 = <_ as Num>::from_i32(42);
// => <_ as Num>::from_i32(42);
let _: f64 = <f64 as Num>::from_i32(42);
// => <f64 as Num>::from_i32(42);
let _: f64 = f64::from_i32(42);
// => <f64>::from_i32(42);
Types inside the qualified path expression can be fully or partially inferred by using _. The trait in <Ty as Trait> cannot be inferred in that way (though its parameters can be). This works similarly to other forms of type annotation.
let iter = (0..10).map(|i| format!("{i}"));
let vec: Vec<_> = iter.collect();
// alt:
// let vec = iter.collect::<Vec<_>>();
The first one is also syntactic sugar, like the fourth one. Only it uses the trait name instead of the type name to determine what function to call. This is helpful when there is a naming conflict with the type itself:
struct F64(f64);
trait Num {
fn from_i32(n: i32) -> Self;
}
impl Num for F64 {
fn from_i32(n: i32) -> F64 { Self(n as f64) }
}
impl F64 {
fn from_i32(n: i32) -> Self {
Self(0.)
}
}
fn main() {
let x: F64 = Num::from_i32(42);
let _: F64 = <_ as Num>::from_i32(42);
let _: F64 = <F64 as Num>::from_i32(42);
let y: F64 = F64::from_i32(42);
assert_eq!(x.0, 42.);
assert_eq!(y.0, 0.);
}
The multiple syntaxes are there because each one either fails-to or can't express some cases.
The more general way to understand them is to think of all as qualified path expressions.
I tried to re-organise with my own words below, adding some few questions.
Case 1:
Syntax: trait::method.
Target: trait implementation.
let _: f64 = Num::from_i32(42);
Useful if ::from_i32 is also implemented inherently or from another trait. Alternative: <f64 as Num> or <_ as Num>.
What I dislike: the code does not make clear that f64 implements Num. Only the type annotation does. Is this just me here or others too?
Case 2-3:
Syntax: Qualified Path or "as" syntax.
Target: Same as above, a sp trait method.
let _: f64 = <f64 as Num>::from_i32(42);
Alternative with <_ as Num>
Nothing to say about this one; I guess this is useful in some specific cases.
Case 4: inherent method
let _: f64 = f64::from_i32(42);
Prioritise an inherent implementation rather than traits.
My only complain here is that it'd be nice to have a super general notation that also allows<f64 as f64> for that, but since f64 isn't a trait this fails.
Misc.
As said in the Case 1 is that the type inference is being used to select an actual type for a method, but it's not a code-line, but a type annotation.
Is there a more explicit description (but for beginners) of how to make sense of the f64 on the left being used on the right to assign the Self ? (more precisely for the 1st expression) that seems to be a big part of why it was confusing to me.
When it is on the right hand side, for some reason it seems easier.