[No Audio]
In this video tutorial, we will be covering
four topics. These topics include scope of a
trait, trait methods with same name, super
traits, marker traits, and auto traits.
Pretty much exciting material to cover, so
let's get started. First, we will talk about
the scope of a trait. The scope of a trait
refers to the portion of the code, where the
trait is visible, and where its methods are
accessible. When a trait is defined in a
module, it is only visible and usable within
that module, and any nested modules within
that particular module. If the trait is
marked as pub, it can, it can also be used by
other modules outside of its scope.
Understanding the scope of a trait is
important for organizing Rust code, and for
ensuring that traits and their methods are
only used where they are intended to be used,
and not accidentally exposed to other parts
of the code where they shouldn't be exposed.
With this background, let us dive into an
example for illustrating the scope of a trait.
I will start by adding a library crate.
[No Audio]
I will declare a module of basic.
[No Audio]
This module will have a public personal_info trait,
which will contain a function of
how_old which will return the age as a u8.
[No Audio]
Next, I will define a public structure of
Student containing a couple of fields of name and age.
[No Audio]
Next, we will implement the personal_info.
[No Audio]
Let us include the definition
of how_old function now.
[No Audio]
Next, we will define some_fn, which will
make use of the how_old function
within the same module.
[No Audio]
I will define an instance with some values.
[No Audio]
Next, I will call the function
of how_old on this instance.
[No Audio]
Now if I remove the pub keyword
from the trait, the trait is still in scope,
and therefore there won't be any issues.
However, if I keep the trait private to the
module, and use the same function outside the
module, the compiler will not be happy. Let
me copy the code of the function and paste it
outside the module.
[No Audio]
I will first bring the relevant items
from the module into scope of
the function. This is because, they will not
be in scope by default, because they are
residing in a different module. You may note
that there are errors indicating to us that
the trait personal_info is private. This
means that by default, the trait is only
available to us within the module or sub
modules of the main module in which it is
defined. Let us go ahead and make the trait public.
[No Audio]
The compiler is now happy and has no issues.
It is not always necessary and
mandatory to have the trait definition and
its implementation for a specific type to be
always in the same module. The trait
definition may be in one module, and its
implementation may be in another module.
We just need to make sure that module in which
we intend to have implementation of a trait
for a specific type must bring the trait into
its scope. For instance, I will define another module.
[No Audio]
I will bring the personal_info trait into scope first.
[No Audio]
Next I will define another struct called Person.
[No Audio]
I will implement the personal_info for Person.
[No Audio]
Finally, I will add the definition of how_old function.
[No Audio]
This will work fine, as long as the
personal_info is public. However,
if we make the personal_info private,
then it will not work. Let me make it private.
The compiler is indicating to us some errors.
The summary is that like structs, functions,
and enums, we can also make the traits public
or private for controlling its scope and
visibility inside the code. To get around the
error in this case, we can either make the
trait public or move the module inside the
basic module. Both the options are available
and are dependent on your underlying design,
and you have to decide it carefully for
proper organization of your code. Let us make
it public for now. Finally, the same rules
will apply if we intend to use the trait of
personal_info in the main program.
This is quite obvious, so we will leave the
details of that and we'll move to the next topic.
The next topic is with regards to
trait methods with the same name for the
type. When we have a trait with a method
which is implemented for a specific type, and
the type itself also has an implementation
for the same method, then the compiler
defaults to calling the method that is
directly implemented on the type itself.
Let us see the details with an example. Consider a
Pilot trait with a method of fly.
[No Audio]
Let us define another trait of Wizard also
containing the fly method.
[No Audio]
Next, let us define a Human
struct with no fields.
[No Audio]
Let us now implement the two traits for the Human.
First, we will implement the Pilot trait for Human.
[No Audio]
Let us say the fly method definition now.
[No Audio]
Next, we will implement the Wizard for Human.
[No Audio]
Let us say the function of fly.
[No Audio]
Finally, we will directly implement the fly
inside the implementation of Human.
[No Audio]
You may have noticed that, now we have three
implementations of the same function of fly
for Human in three different contexts of Pilot,
Wizard, and a direct implementation inside the
Human. As pointed out earlier, when the type
itself implements a method, so by default, its
own version will be invoked. In this case, we
are expecting the fly within the Human
implementation to be invoked. Let us see if
this is the case. In the main, I will first
create an instance of the Human.
[No Audio]
Next, I will call the fly on the Human.
Let us execute to see which function
will be invoked in this case.
[No Audio]
As expected, the function inside the
implementation of the type itself has been
invoked. To invoke the specific
implementations for the type. We will not use
the dot notation, but rather will use the full
information of the function with proper
inputs. For instance, to invoke the
implementation provided by the pilot trait, we
will use this syntax.
[No Audio]
In the same way, to invoke the
implementation inside the Wizard
trait, we will use this syntax.
[No Audio]
Let us now move to the
next topic of super traits, but first
I will comment out the code.
[No Audio]
Rust doesn't have inheritance, but you can
define a trait as being a super trait of
another trait. Super traits are a feature of
Rust's trait system that allows one trait to
inherit methods and associated types from
another trait. When a trait inherits from
another trait, it gains access to all the
methods and associated types of the parent
trait. This is similar to inheritance in
object oriented programming in other
languages, where a subclass can inherit
methods and properties from a parent class.
Let us see some examples to illustrate the
key ideas. I will define a trait of person
containing a function of name.
[No Audio]
Next, I will define another trait called
Student with a Person being it's super trait.
[No Audio]
The colon followed by the trait after the
name of the trait, is the syntax for
mentioning a super trait for the trait. In
this case the Person is a super trait for the
Student trait. The key requirement for any
type now that implements the Student trait
is that, it also needs to have an
implementation for the Person trait. This
means that, any type which implements the
Student trait must also implement the Person
trait. Let us include a function of complete_info
inside the Student trait with an
input of self and an output of a tuple.
[No Audio]
Let us now see how we can implement the trait
for a specific type. I will define a
university_student struct.
[No Audio]
The structure will have three fields.
[No Audio]
Next we will implement the Student for university student.
[No Audio]
Let us include the function of complete_info.
[No Audio]
The function will return a tuple with the first value being the
name, the second value being the age, and the
third value being the university of the Student.
[No Audio]
You may note that we do not have the name
method implemented for the Student for now.
This method will be used from the parent
trait implementation, so this means that we
also need to implement the parent trait for
the type of university_student.
There is also an error that is being
indicated by the compiler, let us see the
details. It says the trait bound uni_student: Person
is not satisfied, the trait person is not
implemented for uni_student.
The compiler is also indicating to
use the Person trait which is not implemented
for the type. Another way of thinking about
the parent trait is that, when we declare a
trait to be a parent trait, then in some
sense, we are associating some extra
properties with the type, which also needs to
be satisfied for that particular type.
In other words, the type must also have these
additional properties, let us implement the parent trait now.
[No Audio]
Next, let us also include the method of name
which will return the name of the Student.
[No Audio]
Now, the compiler is no more complaining as
the requirements are being fulfilled, that is,
any type which implements the child trait
must also implement a parent trait or super
trait. Let us use these implementations in
the main now. I will create an instance of
the university Student initialized from
some suitable values.
[No Audio]
I will now call the complete_info
function, which will enable us to use the
parent track method of name.
[No Audio]
I can also directly call the parent trait methods,
since they are also implemented for the type.
[No Audio]
Now, any type which is Student can access the
super trait Person methods. To see this more
clearly, I will define a function called info.
[No Audio]
This function we'll use a generic type s
which will have the Student trait.
[No Audio]
The input to the function will be a reference to s.
[No Audio]
Inside the function, I will access the
complete_info which is implemented for the
Student inside the print statement.
[No Audio]
I will also access the name function which is not
part of the Student trait, but it's part of
the super trait Person.
[No Audio]
This means now, that the
super trait methods are also
accessible to us, although the generic is of type
Student and not that of a Person. Let us call the info
function in the main now with the reference to s.
[No Audio]
Let us cargo run this now.
[No Audio]
With regards to the super traits, there is a
closely related concept of combo traits.
Combo traits refers to the use of multiple
traits and capacity of super traits. Let us
extend the current example to explain this.
Let me add one more trait called Programmer,
which will have a single function of favorite language.
[No Audio]
Next, I will define another trait
called computer science student, and we'll
mention multiple traits as its super traits.
[No Audio]
The plus means that the trait computer
science student will have both the Programmer
and the Student as its super traits. This is
also referred to as combo traits, which is
used to mention multiple traits as super
traits. Please note now that in this case,
implementing the computer science student for
a certain type will require us to implement
both the super traits of Programmer and
Student. Moreover, since Student has a super
trait of person, so implementing type must
also have the implementation for the Person trait.
Let us now move to the topic of marker
traits, but first I will comment out the code.
[No Audio]
A marker trait is a trait that doesn't require
any methods to be implemented. Moreover, it
also does not have any trait items. Instead,
it serves as a way to add metadata or
constraints to a type. Marker traits are
useful in Rust, because they allow us to
commute additional information
about a type to the compiler without
requiring any actual functionality to be
implemented. This can be helpful in cases
where we need to specify certain behaviors or
properties of a type without actually
providing any concrete implementations.
Let us see an example of this. I will define a
trait called Some_properties,
which will have Clone, PartialEq, and
Default as its super traits.
[No Audio]
The plus is used to mention the combo traits
and the colon in the syntax for mentioning
the super traits. This means now that
all these traits are the super traits
have the Some_properties traits.
Now, as pointed out a few
times earlier also, any type which has the
Some_properties trait will also
have the Clone, PartialEq, and the
Default traits. This means now that by
declaring a type to have Some_properties,
we are putting a restriction on
this type or marking the type that it must
also have the Clone, PartialEq, and
Default traits. Let us declare a Student struct next.
[No Audio]
The structure will have three fields.
[No Audio]
Since the Some_properties
does not have anything to being implemented,
so therefore it just serve as a marker.
Next, we will implement the sum underscore
properties for Student.
[No Audio]
You may note that the compiler is not happy,
it says the trait bound Student
call on Default is not satisfied. The
trait Default is not implemented for Student,
let us add the Default.
[No Audio]
You may note that it is still complaining,
it will keep on complaining until
and unless we derive all
the required traits for the type. So let us
look at all the required traits.
[No Audio]
The code is now compiling. In summary, the marker traits
are useful for mentioning some constraints on
a type. Let us now cover the last topic,
which is the auto traits, but first, I will
comment out the code.
[No Audio]
Auto traits are traits that get
automatically implemented for a type, if all
of its members also implement the trait. What
member means depends on the type, for
example, fields of a struct or variants of an
enum. Let us see an example to understand
auto traits. Consider a simple customer
struct with two fields.
[No Audio]
Let us add the Default
trait to this type by deriving it.
[No Audio]
There were no issues, because the members have
the type, which in this case are the fields of
the struct also have the implementation for
the Default. In fact, all the primitive types
implements the Default trait. This means that
the Default trait is an auto trait for the
customer. Since all of its types also
implements this trait, however, if we add
another type to it, which which does not have
the default implementation, then the compiler
will not be happy and it will not consider it
to be an auto trait. Let me add another field
of relationship to the customer which will be
an enum with type visit.
[No Audio]
Let us define the enum now.
[No Audio]
In order to make the Default as an
auto trait, we will need to implement the
Default for the Visit type also, so let us add this.
[No Audio]
The Default trait contains a single
function of Default, so let us add that.
[No Audio]
The function will return a variant of new as a
Default value. Now you may note that the
compiler has no issues, we can now use this
function to initialize an instance
of the struct in the main.
[No Audio]
With this we end this tutorial, I hope you
would have enjoyed this. See you again and
until next tutorial, enjoy Rust programming