How to make these Trait calls a bit more intuitive? Or why do they work?

In the reference there is the snippet:

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.

My simplistic view:

  • 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);
}

Playground.

1 Like

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.

but what about the first one?

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<_>>();
2 Likes

Yes, _ is called the inferred type.

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.);
}

Playground.

You see this syntax often being used with Default::default().

2 Likes

Thanks for the explanations, links and code snippets: @jumpnbrownweasel , @jofas , @quinedot.

My takeaways were:

  • 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.

Or more tersely, if the above is too long to read:

// #[derive(Default)]
struct SomeOptions {
    foo: i32,
    bar: f32,
}

fn main() {
    // qualified syntax
    let options = <SomeOptions as Default>::default();
}

that's more clear to me than

// ...
fn main() {
    // trait::method syntax
    let options: SomeOptions = Default::default();
}

I'm just trying to see whether something can be done to make me understand better and finally hatch :hatching_chick:

I feel like type annotations shouldn't be part of the actual code in the way it is there.


PS: I never got to the bottom of Default::default() and didn't know we could implement it for my structs ! That's useful.