[No Audio]
In this tutorial, we will be talking about
trait items. Some of these items are already
somewhat covered in some of the previous
tutorials, so we will go through them
quickly. The trait items are any items that
are part of a trait declaration. This means
that all the functions, associated types or
generics that are declared as part of the trait
declaration are trait items. Within the
context of trait items, the first concept is
that of the Self which generally confuses
beginner level programmers. Let us develop a
good understanding of what exactly do we mean
by Self, I will define a simple trait
containing a couple of functions.
[No Audio]
The first function returns a simple i32
value, and the second function returns the
implementation type which is indicated by the
Self keyword. Within this context, it means the
type for the simple trait which is being
implemented. Let us now define a couple of unit structs.
[No Audio]
A unit struct is a special
struct in Rust programming language with no fields.
Next, let us define the simple trait for the SomeType.
[No Audio]
The fn_2 will return an
instance of the Self, which
refers to the implementing type, that is the
SomeType itself within this implementation block.
[No Audio]
This means that, the Self means the
type for which we are implementing the trait
and is context dependent. Let us see it more
clearly by implementing the trait for the
other type. For the fn_1, we will
again return some arbitrary value.
[No Audio]
For the second function, we will return other type.
[No Audio]
As you may have spotted, the meaning of Self
in this context is now the other type, which
is the type for which we are implementing the
trait. Now, let us move to the second
topic, which is with regards to the trait
items of functions and methods. First, I will
comment out the previous code.
[No Audio]
We will start by highlighting
the key difference between functions and methods
within the context of traits.
A trait bound function is any
function, whose first parameter does not use
the Self keyword. A trait_method is any
function whose first parameter uses the Self.
Let us go through some examples to clearly
understand the difference, I will define a
simple trait of Default.
I will add the default function which will return the Self.
[No Audio]
You may note that this is a function, since the
first parameter is not that of Self. Let us
implement this for an i32 value.
[No Audio]
The function will return a Default value of
zero in this case. Let us use the
implementation now in the main. I will call the function.
[No Audio]
The Rust compiler will complain
if we do not provide the type, since there is
no way to tell Rust that I am going to call
this method for an i32 type, let us provide
the type now. The function with no Self
argument cannot be called like methods, for
instance, I will define an i32 variable.
Now, let us use the dot notation to
access the function. As expected, the compiler
is not happy. Let us inspect the error,
it says, to be used as methods function and must
have a self parameter. Now, let us look at
some examples of trait_methods, I will add another trait.
[No Audio]
As pointed out earlier, a trait_method
is any function whose first parameter
uses the Self. The Self can appear as an own
type, a reference to Self, or a mutable
reference to Self. Let me add some functions to explain this.
[No Audio]
The first function is using
an owned Self type, the second one is using a
reference to Self, and the third is using a
mutable reference to Self. The first function
can be desugared into Self with a type of Self.
[No Audio]
Similarly, the second function can be
desugared into Self with a type of reference to Self.
The third one is similarly desugared
into Self with a type of mutable reference to
Self, and similarly, this small self refers to
instance for which the function is being
called when the Self with a capital refers to
the type itself which can be owned reference
or unmutable reference. We have used
both the methods and functions in the
BlockChain implementation section of the
course. Let us see some examples of that in
the code. I will comment out this code first.
[No Audio]
Next I will paste the code
of the BlockChain implementation.
[No Audio]
In the BlockChain implementation,
you may have spotted that the
new is a function and not a method, however,
the starting block is a method. Similarly
try_add_block is also a
method. In the same way, is_block_valid
is also a method, the remaining functions in the same
implementation blocks are also methods.
However, in the block implementation, the new
and mine, are both functions and not
methods. Now, let us move to the most
important and key concept, that is generic
parameters and associated types and their
relationship within the context of traits.
I will comment out the code first and we'll start fresh.
[No Audio]
Both generic types and
associated types defer the decision to the
implementer on which concrete types should be
used in the trait functions and methods.
In these examples, we seek to explain when to use
one over the other within the trait. The
general rule of thumb is, use associated types
when there should be only a single
implementation of the trait per type and use
generic types when there can be many possible
implementations of the trait per type. Now,
let's see an example to explain this in some
detail. Let us define the add trait
with some associated types.
[No Audio]
The trait will also contain an add function
with the inputs of Self, and Rhs or right hand
side, and the output type will be Self::Output.
[No Audio]
This is a general implementation of the trait
add, which will let us add some sort of two
things where one will be called the left hand
side, and the other one will be called a right
hand side. The Self for the associated type
means that, these associated types will be
defined based on the type for which the trait
is being implemented, let us define a point struct.
[No Audio]
This structure will contain the x and
y coordinates as fields.
[No Audio]
We will now implement the add trait for the point type.
[No Audio]
First, we will set the type of the right hand
side or Rhs to that of a point. The output
will also be a point, so let us set its type also.
[No Audio]
Finally, we will add a function with
suitable inputs and outputs.
[No Audio]
We will return a point
with the x axis value of Self
being added to that of the right hand side x
axis value, and the y axis value of Self
being added to that of the right hand side of
y axis value. We can also use the Rhs to be,
to be of type point since Self::Rhs
has a type of point in this case.
In the same way, we can also use point as the
return type, since Self::Output
means a type of point in this case.
[No Audio]
Let us now use this in the main,
I will create a couple of points.
[No Audio]
Next, I will call the add function
[No Audio]
That is now confirmed that the x axis value
and the y axis values are equal to 3.
[No Audio]
Let us cargo run this now.
[No Audio]
This program is fine. However, let's say we
want it to add the ability to add i32
values two point, where i32 would
be added to both the x and y members.
Let us go ahead and create another implementation
for the Add for Point, where Rhs will be an i32 value.
[No Audio]
We will set the type of RHS to date of an
i32 value and the output will be a point.
[No Audio]
The right hand side or Rhs will be added to
the x axis value, and y axis value inside the Add function.
[No Audio]
You may note that the compiler is not happy,
it says conflicting implementation of trait
add for type point. Since the add trait is
not parameterised by any generic type, so we
can only implement it once per type, which
means, we can only pick the types for both Rhs
and output once. To allow editing both points
in i32 values to point, we have to
refactor Rhs from an associated type to a
generic type, which would allow us to
implement a trait multiple times for point
with different type arguments for Rhs.
So, let us change the Rhs from an associated type
to that of a generic type.
First, we will change the definition of the trait by
including a generic parameter of RHS.
[No Audio]
Next, we will comment out the Rhs type, since it is
now being treated as generic. Next, we will
remove the Self from the Rhs since it is now
being treated as generic and not tied with
the type itself for which we are implementing
the trait anymore. Next, in the first
implementation of the add, we will set the
generic to that of a point, since this
implementation is for the type point, where
the generic should be treated as point.
[No Audio]
We will also remove the type Rhs equals to
Point since that trait is no more using the
Rhs associated type. In the same way, in the
second implementation of the add, we will set
the generic type to that of an i32 to let
the compiler know that in this
implementation, a generic of i32 should
be used. Again we will remove the associated
type, as it is no more required. You may have
noted that, the generic allows us to have
multiple implementation of the same trait for
a single type, which is the type of point in
this case. With associated type, it was not
possible. In general, we will use the
associated types when there is only a single
implementation of the trait per type.
However, if there are multiple
implementations of the same types for a
trait, then we will use the generics. Let us
now use the implementation in the main, I will
define a point first.
[No Audio]
Next, I will add an
integer value to the point by calling the
add function which is defined in the new
implementation. Finally, let us use the
assert to check if the value is updated correctly or not.
[No Audio]
Let us cargo run this now.
[No Audio]
Everything is fine up till now. Let us
further extend the implementation by
considering another case. Let's say we want
to add a new type called line which contains
two points. And now, there are there are some
contexts within our program where adding two
points should produce a line instead of a
point. This means that the output is not
necessarily, is not necessarily a point
anymore, but can also be aligned. This is not
possible, given the current design of the a
trait where output is an associated type, but
we can satisfy these new requirements by
refactoring output from an associated type
into generic type. Let us see how we can do this.
Since the output can now be different
in the different implementations, so
therefore, we will consider it a generic
type. So, let us change the definition of the trait.
[No Audio]
We will delete it as associated
type from the trait definition.
[No Audio]
Finally, we will also remove
the cell from the output.
In the implementation now, we will use two generics
instead of one. In the first implementation,
both the Rhs and the output are points.
[No Audio]
We do not need the associated type of
output in the trait implementation.
In the second implementation, the Rhs
is i32 and the output is a point.
[No Audio]
We will also remove the
associated type of output.
Now, to take care of the new requirements,
we will first add a line struct
with two fields of start and
end, both of which will be of type Point.
[No Audio]
Next, we will add another implementation for
adding a point to another point, the Rhs in
this case will be a Point and the output will be a line.
[No Audio]
We will next add the definition for the add function.
[No Audio]
The input will be Self and
the rhs and the output will be a line.
[No Audio]
We will return a Line with the start being equal
to the point of Self, and the end being equal
to the point and indicated by the right hand
side. Let us now use it in the main.
I will declare a couple of points.
[No Audio]
Next, I will call the add function.
[No Audio]
Please note that in this case, when I pass the
parameter of p2, which is a point to the
function, so both the implementation number
one and implementation number three matches,
where the right hand side is a point. This is
because, in both the implementations, the
function accepts a Self and a Point, so
therefore, the compiler is confused. We need
to somehow tell the compiler that I need to
invoke the implementation number three, where
I should get back a Line and not a Point.
This tells the compiler now, that the user of
the implementation wants to invoke the
implementation, where the input is a point, and
the output he is expecting is that of a line.
In the same way, we will also need to mention
the output type in the first call to the
add function to help the compiler identify
the correct implementation that needs to be
invoked. Finally, I will add some assertion
to make sure that our code is correct.
[No Audio]
Let us cargo run this now.
[No Audio]
With this, we end this
tutorial. The key point to note is that, we
need to use the associated types
when there should only be a single
implementation of the trait per type.
However, we will prefer to use the generic
types, when there can be many possible
implementations of the trait per type. See you again
and until next tutorial, happy Rust programming.