Traits
Overview
Traits in Noir are a useful abstraction similar to interfaces or protocols in other languages. Each trait defines the interface of several methods contained within the trait. Types can then implement this trait by providing implementations for these methods. For example in the program:
struct Rectangle {
width: Field,
height: Field,
}
impl Rectangle {
fn area(self) -> Field {
self.width * self.height
}
}
fn log_area(r: Rectangle) {
println(r.area());
}
We have a function log_area
to log the area of a Rectangle
. Now how should we change the program if we want this
function to work on Triangle
s as well?:
struct Triangle {
width: Field,
height: Field,
}
impl Triangle {
fn area(self) -> Field {
self.width * self.height / 2
}
}
Making log_area
generic over all types T
would be invalid since not all types have an area
method. Instead, we can
introduce a new Area
trait and make log_area
generic over all types T
that implement Area
:
trait Area {
fn area(self) -> Field;
}
fn log_area<T>(shape: T) where T: Area {
println(shape.area());
}
We also need to explicitly implement Area
for Rectangle
and Triangle
. We can do that by changing their existing
impls slightly. Note that the parameter types and return type of each of our area
methods must match those defined
by the Area
trait.
impl Area for Rectangle {
fn area(self) -> Field {
self.width * self.height
}
}
impl Area for Triangle {
fn area(self) -> Field {
self.width * self.height / 2
}
}
Now we have a working program that is generic over any type of Shape that is used! Others can even use this program
as a library with their own types - such as Circle
- as long as they also implement Area
for these types.
Where Clauses
As seen in log_area
above, when we want to create a function or method that is generic over any type that implements
a trait, we can add a where clause to the generic function.
fn log_area<T>(shape: T) where T: Area {
println(shape.area());
}
It is also possible to apply multiple trait constraints on the same variable at once by combining traits with the +
operator. Similarly, we can have multiple trait constraints by separating each with a comma:
fn foo<T, U>(elements: [T], thing: U) where
T: Default + Add + Eq,
U: Bar,
{
let mut sum = T::default();
for element in elements {
sum += element;
}
if sum == T::default() {
thing.bar();
}
}
Trait bounds
A shorter syntax for specifying trait bounds directly on generic types is available. For example, this code:
fn log_area<T>(shape: T) where T: Area {
println(shape.area());
}
can also be written like this:
fn log_area<T: Area>(shape: T) {
println(shape.area());
}
Both are equivalent. Using where
is preferable when there are many trait bounds and it's clearer to have
them separate from the types the are applying bounds to.
Invoking trait methods
As seen in the previous section, the area
method was invoked on a type T
that had a where clause T: Area
.
To invoke area
on a type that directly implements the trait Area
, the trait must be in scope (imported):
use geometry::Rectangle;
fn main() {
let rectangle = Rectangle { width: 1, height: 2};
let area = rectangle.area(); // Error: the compiler doesn't know which `area` method this is
}
The above program errors because there might be multiple traits with an area
method, all implemented
by Rectangle
, and it's not clear which one should be used.
To make the above program compile, the trait must be imported:
use geometry::Rectangle;
use geometry::Area; // Bring the Area trait into scope
fn main() {
let rectangle = Rectangle { width: 1, height: 2};
let area = rectangle.area(); // OK: will use `area` from `geometry::Area`
}
An error will also be produced if multiple traits with an area
method are in scope. If both traits
are needed in a file you can use the fully-qualified path to the trait:
use geometry::Rectangle;
fn main() {
let rectangle = Rectangle { width: 1, height: 2};
let area = geometry::Area::area(rectangle);
}
As Trait Syntax
Rarely to call a method it may not be sufficient to use the general method call syntax of obj.method(args)
.
One case where this may happen is if there are two traits in scope which both define a method with the same name.
For example:
trait Foo { fn bar(); }
trait Foo2 { fn bar(); }
fn example<T>()
where T: Foo + Foo2
{
// How to call Foo::bar and Foo2::bar?
}
In the above example we have both Foo
and Foo2
which define a bar
method. The normal way to resolve
this would be to use the static method syntax Foo::bar(object)
but there is no object in this case and
Self
does not appear in the type signature of bar
at all so we would not know which impl to choose.
For these situations there is the "as trait" syntax: <Type as Trait>::method(object, args...)
fn example<T>()
where T: Foo + Foo2
{
<T as Foo>::bar();
<T as Foo2>::bar();
}
Generic Implementations
You can add generics to a trait implementation by adding the generic list after the impl
keyword:
trait Second {
fn second(self) -> Field;
}
impl<T> Second for (T, Field) {
fn second(self) -> Field {
self.1
}
}
You can also implement a trait for every type this way:
trait Debug {
fn debug(self);
}
impl<T> Debug for T {
fn debug(self) {
println(self);
}
}
fn main() {
1.debug();
}