Traits in Cairo
Traits specify functionality blueprints that can be implemented. The blueprint specification includes a set of function signatures containing type annotations for the parameters and return value. This sets a standard to implement the specific functionality.
Defining a Trait
To define a trait, you use the keyword trait
followed by the name of the trait in PascalCase
then the function signatures in a pair of curly braces.
For example, let's say that we have multiple structs representing shapes. We want our application to be able to perform geometry operations on these shapes, So we define a trait ShapeGeometry
that contains a blueprint to implement geometry operations on a shape like this:
trait ShapeGeometry {
fn boundary(self: Rectangle) -> u64;
fn area(self: Rectangle) -> u64;
}
Here our trait ShapeGeometry
declares signatures for two methods boundary
and area
. When implemented, both these functions should return a u64
and accept parameters as specified by the trait.
Implementing a Trait
A trait can be implemented using impl
keyword with the name of your implementation followed by of
then the name of trait being implemented. Here's an example implementing ShapeGeometry
trait.
impl RectangleGeometry of ShapeGeometry {
fn boundary(self: Rectangle) -> u64 {
2_u64 * (self.height + self.width)
}
fn area(self: Rectangle) -> u64 {
self.height * self.width
}
}
In the code above, RectangleGeometry
implements the trait ShapeGeometry
defining what the methods boundary
and area
should do. Note that the function parameters and return value types are identical to the trait specification.
Parameter self
In the example above, self
is a special parameter. When a parameter with name self
is used, the implemented functions are also attached to the instances of the type as methods. Here's an illustration,
When the ShapeGeometry
trait is implemented, the function area
from the ShapeGeometry
trait can be called in two ways:
let rect = Rectangle { ... }; // Rectangle instantiation
// First way, as a method on the struct instance
let area1 = rect.area();
// Second way, from the implementation
let area2 = RectangleGeometry::area(rect);
// `area1` has same value as `area2`
area1.print();
area2.print();
And the implementation of the area
method will be accessed via the self
parameter.
Generic Traits
Usually we want to write a trait when we want multiple types to implement a functionality in a standard way. However, in the example above the signatures are static and cannot be used for multiple types. To do this, we use generic types when defining traits.
In the example below, we use generic type T
and our method signatures can use this alias which can be provided during implementation.
use debug::PrintTrait;
// Here T is an alias type which will be provided buring implementation
trait ShapeGeometry<T> {
fn boundary(self: T) -> u64;
fn area(self: T) -> u64;
}
// Implementation RectangleGeometry passes in <Rectangle>
// to implement the trait for that type
impl RectangleGeometry of ShapeGeometry<Rectangle> {
fn boundary(self: Rectangle) -> u64 {
2_u64 * (self.height + self.width)
}
fn area(self: Rectangle) -> u64 {
self.height * self.width
}
}
// We might have another struct Circle
// which can use the same trait spec
impl CircleGeometry of ShapeGeometry<Circle> {
fn boundary(self: Circle) -> u64 {
(2_u64 * 314_u64 * self.radius) / 100_u64
}
fn area(self: Circle) -> u64 {
(314_u64 * self.radius * self.radius) / 100_u64
}
}
fn main() {
let rect = Rectangle { height: 5_u128, width: 7_u128 };
rect.area().print(); // 35
rect.boundary().print(); // 24
let circ = Circle { radius: 5_u128 };
circ.area().print(); // 78
circ.boundary().print(); // 31
}
Managing and using external trait implementations
To use traits methods, you need to make sure the correct traits/implementation(s) are imported. In the code above we imported PrintTrait
from debug
with use debug::PrintTrait;
to use print()
methods.
In some cases you might need to import not only the trait but also the implementation if they are declared in separate modules.
If CircleGeometry
was in a separate module/file circle
then to use boundary
on circ: Circle
, we'd need to import CircleGeometry
in addition to ShapeGeometry
.
If the code was organised into modules like this,
use debug::PrintTrait;
// struct Circle { ... } and struct Rectangle { ... }
mod geometry {
use super::Rectangle;
trait ShapeGeometry<T> {
// ...
}
impl RectangleGeometry of ShapeGeometry::<Rectangle> {
// ...
}
}
// Could be in a different file
mod circle {
use super::geometry::ShapeGeometry;
use super::Circle;
impl CircleGeometry of ShapeGeometry::<Circle> {
// ...
}
}
fn main() {
let rect = Rectangle { height: 5_u64, width: 7_u64 };
let circ = Circle { radius: 5_u64 };
// Fails with this error
// Method `area` not found on... Did you import the correct trait and impl?
rect.area().print();
circ.area().print();
}
To make it work, in addition to,
use geometry::ShapeGeometry;
you might also need to use CircleGeometry
,
use circle::CircleGeometry