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();
}
}
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();
}
Generic Trait Implementations With Where Clauses
Where clauses can also be placed on trait implementations themselves to restrict generics in a similar way.
For example, while impl<T> Foo for T
implements the trait Foo
for every type, impl<T> Foo for T where T: Bar
will implement Foo
only for types that also implement Bar
. This is often used for implementing generic types.
For example, here is the implementation for array equality:
impl<T, N> Eq for [T; N] where T: Eq {
// Test if two arrays have the same elements.
// Because both arrays must have length N, we know their lengths already match.
fn eq(self, other: Self) -> bool {
let mut result = true;
for i in 0 .. self.len() {
// The T: Eq constraint is needed to call == on the array elements here
result &= self[i] == other[i];
}
result
}
}
Generic Traits
Traits themselves can also be generic by placing the generic arguments after the trait name. These generics are in scope of every item within the trait.
trait Into<T> {
// Convert `self` to type `T`
fn into(self) -> T;
}
When implementing generic traits the generic arguments of the trait must be specified. This is also true anytime
when referencing a generic trait (e.g. in a where
clause).
struct MyStruct {
array: [Field; 2],
}
impl Into<[Field; 2]> for MyStruct {
fn into(self) -> [Field; 2] {
self.array
}
}
fn as_array<T>(x: T) -> [Field; 2]
where T: Into<[Field; 2]>
{
x.into()
}
fn main() {
let array = [1, 2];
let my_struct = MyStruct { array };
assert_eq(as_array(my_struct), array);
}
Trait Methods With No self
A trait can contain any number of methods, each of which have access to the Self
type which represents each type
that eventually implements the trait. Similarly, the self
variable is available as well but is not required to be used.
For example, we can define a trait to create a default value for a type. This trait will need to return the Self
type
but doesn't need to take any parameters:
trait Default {
fn default() -> Self;
}
Implementing this trait can be done similarly to any other trait:
impl Default for Field {
fn default() -> Field {
0
}
}
struct MyType {}
impl Default for MyType {
fn default() -> Field {
MyType {}
}
}
However, since there is no self
parameter, we cannot call it via the method call syntax object.method()
.
Instead, we'll need to refer to the function directly. This can be done either by referring to the
specific impl MyType::default()
or referring to the trait itself Default::default()
. In the later
case, type inference determines the impl that is selected.
let my_struct = MyStruct::default();
let x: Field = Default::default();
let result = x + Default::default();
let _ = Default::default();
If type inference cannot select which impl to use because of an ambiguous Self
type, an impl will be
arbitrarily selected. This occurs most often when the result of a trait function call with no parameters
is unused. To avoid this, when calling a trait function with no self
or Self
parameters or return type,
always refer to it via the implementation type's namespace - e.g. MyType::default()
.
This is set to change to an error in future Noir versions.
Default Method Implementations
A trait can also have default implementations of its methods by giving a body to the desired functions.
Note that this body must be valid for all types that may implement the trait. As a result, the only
valid operations on self
will be operations valid for any type or other operations on the trait itself.
trait Numeric {
fn add(self, other: Self) -> Self;
// Default implementation of double is (self + self)
fn double(self) -> Self {
self.add(self)
}
}
When implementing a trait with default functions, a type may choose to implement only the required functions:
impl Numeric for Field {
fn add(self, other: Field) -> Field {
self + other
}
}
Or it may implement the optional methods as well:
impl Numeric for u32 {
fn add(self, other: u32) -> u32 {
self + other
}
fn double(self) -> u32 {
self * 2
}
}
Impl Specialization
When implementing traits for a generic type it is possible to implement the trait for only a certain combination of generics. This can be either as an optimization or because those specific generics are required to implement the trait.
trait Sub {
fn sub(self, other: Self) -> Self;
}
struct NonZero<T> {
value: T,
}
impl Sub for NonZero<Field> {
fn sub(self, other: Self) -> Self {
let value = self.value - other.value;
assert(value != 0);
NonZero { value }
}
}
Overlapping Implementations
Overlapping implementations are disallowed by Noir to ensure Noir's decision on which impl to select is never ambiguous.
This means if a trait Foo
is already implemented
by a type Bar<T>
for all T
, then we cannot also have a separate impl for Bar<Field>
(or any other
type argument). Similarly, if there is an impl for all T
such as impl<T> Debug for T
, we cannot create
any more impls to Debug
for other types since it would be ambiguous which impl to choose for any given
method call.
trait Trait {}
// Previous impl defined here
impl<A, B> Trait for (A, B) {}
// error: Impl for type `(Field, Field)` overlaps with existing impl
impl Trait for (Field, Field) {}
Trait Coherence
Another restriction on trait implementations is coherence. This restriction ensures other crates cannot create impls that may overlap with other impls, even if several unrelated crates are used as dependencies in the same program.
The coherence restriction is: to implement a trait, either the trait itself or the object type must be declared in the crate the impl is in.
In practice this often comes up when using types provided by libraries. If a library provides a type Foo
that does
not implement a trait in the standard library such as Default
, you may not impl Default for Foo
in your own crate.
While restrictive, this prevents later issues or silent changes in the program if the Foo
library later added its
own impl for Default
. If you are a user of the Foo
library in this scenario and need a trait not implemented by the
library your choices are to either submit a patch to the library or use the newtype pattern.
The Newtype Pattern
The newtype pattern gets around the coherence restriction by creating a new wrapper type around the library type
that we cannot create impl
s for. Since the new wrapper type is defined in our current crate, we can create
impls for any trait we need on it.
struct Wrapper {
foo: dep::some_library::Foo,
}
impl Default for Wrapper {
fn default() -> Wrapper {
Wrapper {
foo: dep::some_library::Foo::new(),
}
}
}
Since we have an impl for our own type, the behavior of this code will not change even if some_library
is updated
to provide its own impl Default for Foo
. The downside of this pattern is that it requires extra wrapping and
unwrapping of values when converting to and from the Wrapper
and Foo
types.