Question about trait

My question is about traits in Rust. As you can see from the code below I have 4 methods for printing "Hello world!".

print_hello_world_1:
The input is a reference to struct B. But as you can see the hello_world(&self) has redundant implementation. But I can't use the implementation of hello_world(&self) from trait A. why? If it is because orphan rules, can you explain why? How can I avoid this kind of redundancy in my implementation.

print_hello_world_2:
The input is a reference to trait A, sounds good!

print_hello_world_3:
As you can see it's commented. The size of stack is not know in compile time, Fairly understandable. But the time of calling this function, compiler can check that it is called from struct B and size is known.

print_hello_world_4:
Why here I don't get the same error as number 3? Any explanation why it is different than number 3.

mod foo {
    pub trait A {
        fn hello_world(&self) -> String;
    } 
}

mod bar {
    pub struct B {}
    
    impl super::foo::A for B {
        fn hello_world(&self) -> String {
            "Hello world!".to_string()
        }
    }
    
    impl B {
        // redundant implementation
        pub fn hello_world(&self) -> String {
            "Hello world!".to_string()
        }
    }
}


fn print_hello_world_1(b: &bar::B) {
    println!("{}", b.hello_world());
}

fn print_hello_world_2(a: &dyn foo::A) {
    println!("{}", a.hello_world());
}

// size is not known at compile time
// fn print_hello_world_3(a: foo::A) {
//     println!("{}", a.hello_world());
// }

fn print_hello_world_4(i: impl foo::A) {
    println!("{}", i.hello_world());
}


fn main() {
    let b = bar::B{};

    print_hello_world_1(&b);
    print_hello_world_2(&b);
    //print_hello_world_3(&b);
    print_hello_world_4(b);
}

You can find the code in rust playground here

I appreciate your help.

Just saying a: foo::A makes a trait object using the deprecated so-called "bare trait syntax". It's equivalent to a: dyn foo::A. So you are trying to put a trait object, which is unsized, directly in the function argument. Since trait objects are dynamic, their underlying type is not known even at compile time (that is the point of trait objects, in fact). So the compiler can't go "let me check what size this type has based on the caller".

On the other hand, impl foo::A is not a trait object. It is basically equivalent with ordinary generics except the compiler internally tracks its concrete type. So it's like saying fn print_hello_world_4<T: foo::A>(i: T).

1 Like

It isn't quite clear what you mean by this, but I suspect what you mean is that, if you comment out the impl B {} block with the redundant implementation, you get an error for print_hello_world_1. The reason for that is that the methods of A are not in scope, because they are in a submodule. To fix it, you need to bring the trait into scope:

use foo::A;

or, explicitly relative to the current module:

use self::foo::A;

or, relative to the crate root:

use crate::foo::A;

This removes the need for the redundant implementation.

This has nothing to do with the orphan rule. The orphan rule is about when you can implement a trait for a given type, and the fact that the compiler doesn't complain about the impl super::foo::A for B block indicates that it isn't being broken here. In general, the orphan rule states that a module can only implement a trait if it contains the trait or the implementing type. Since B is in the same module as its implementation, this is fine in your case.

1 Like

Thanks. That was exactly what I meant. Here is the updated version of the code without redundant implementation.
This raise another question for me:
When I have impl A for B {...} inside module bar, (note that module bar uses module foo), why methods of A are not in scope? In this example methods of trait A for struct B belong to which module: A or B?

I fully understand your point. It make sense because the trait is an abstract thing.

for print_hello_world_4 : the compiler goes "let me check what size this type has based on the caller". right?

Yes, when a function with an argument of generic type is being compiled, and provided that the substitution type checks (i.e. the concrete argument type satisfies the conditions specified by the generic type parameter), then at a lower level, (i.e. a later stage in the compilation), the compiler can and will start substituting the concrete type for the generic type parameter.


As a side note, this is in some sense the opposite of what C++ does with templates. Rust typechecks first, and substitutes second. This means that even if you call a generic function with a known and concrete type, you are not permitted to rely on that knowledge in the function body. It is only permitted to use the type and lifetime bounds that its declaration originally established. E.g. the following would not compile:

fn add_one<T>(x: T) -> T {
    x + 1
}

fn main() {
    println!("{}", add_one(42));
}

Albeit it is "obvious" (to a human) that T stands in for an integer and thus 1 can be added to it, the compiler doesn't care about the concrete instantiations of add_one in the light of the callers it's being invoked from. It only cares about the trait bounds (in the above example, none) which serve as constraints allowing the type to be used for more than just moving it around.

This has the nice effect that once a generic function type checks, it is guaranteed to compile and work with all instantiations (concrete types standing in for generic parameters) that satisfy the constraints, and there will be no unpleasant surprises that you might know from experience with C++ templates, whereby a generic function happens not to work in a setting you anticipated, because you forgot that you can't rely on something in the function body.

2 Likes

To have better understanding about your example, I wrote an example code for C++. As you mentioned in C++ the code works fine, but Rust unable to compile it, because typechecks first and substitutes second and also trait bounds.

C++ code works:

#include <cstdio>

template <class T>
T add_one (T x) {
  return x+1;
}

int main() {
   printf("%d", add_one(1));
   return 0;
}

read more:

Thanks @H2CO3

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.