8. Trait
A struct is a concrete and instantiable type whereas a trait is an abstract type that cannot be instantiated. When we define a trait, we define a set of requirements that do not have a concrete implementation, but is intended to be implemented by a concrete type.
Traits are useful abstractions especially in large a codebase. Users of traits do not need to know what concrete struct implements a particular method, as long as the implementation conforms to the requirements given in the trait. In the following code listing, Flyable
trait requires that whatever struct implements Flyable
must implement fly
method. The function fly_it
does not know the concrete implementations Bird
or Plane
. It takes in its arguments a type that confirms to Flyable
, and calls method in the Flyable
. The actual concrete types are passed to it at the call site.
A struct implicitly conforms to a trait if it implements all the requirements for the trait. This means that we do not need to explicitly specify a confirming trait in the struct declaration; but it is a recommended practice to do so.
trait Flyable:
fn fly(self): ...
struct Bird(Flyable):
fn __init__(inout self): ...
fn fly(self): print("Soar into the sky")
struct Plane(Flyable):
fn __init__(inout self): ...
fn fly(self): print("Jet set go")
fn fly_it[T: Flyable](f: T):
f.fly()
fn main():
fly_it(Bird())
fly_it(Plane())
You may have noticed in the fly_it
definition a square bracket with a parameter being passed within it, i.e. [T: Flyable]
. Mojo allows values to be passed during compile time to a function or method. Only requirement is that the values must be passed within square brackets. Those are called parameters. In other languages, parameters and arguments to functions are interchangeable terms. However, in Mojo those are distinct terms. Parameters are passed during compile time to a function, while arguments are passed at runtime. In the above example, we have passed a compile time parameter T
of the type Flyable
to fly_it
. We then used that type T
as the type of the argument f
, indirectly assigning Flyable
as the type of f
.
The anatomy of a trait and its usage in a struct is shown in the following diagram.
8.1. Implementation and inheritance
A single trait can be implemented by many different types, with the condition that the type ensures that all the requirements defined in the trait is implemented by the type. A type can also inherit more than one trait, with the condition that they implement all the combined requirements from of all of those traits within the type itself.
trait Flyable:
fn fly(self): ...
trait Walkable:
fn walk(self): ...
struct Bird(Flyable, Walkable):
fn __init__(inout self): ...
fn fly(self): print("Fly to the sky")
fn walk(self): print("Walk on the ground")
struct Cat(Walkable):
fn __init__(inout self): ...
fn walk(self): print("Walk carefully")
fn main():
Bird().fly()
Bird().walk()
Cat().walk()
Traits can inherit from other traits. They can also inherit multiple traits at the same time.
trait Flyable:
fn fly(self): ...
trait Walkable:
fn walk(self): ...
trait WalkableFlyable(Flyable, Walkable): ...
struct Bird(WalkableFlyable):
fn __init__(inout self): ...
fn fly(self): print("Fly to the sky")
fn walk(self): print("Walk on the ground")
fn main():
Bird().fly()
Bird().walk()
8.2. Static methods
Unlike many other programming languages, Mojo allows traits to require static methods on the structs it defines.
trait Message:
@staticmethod
fn default_message(): ...
struct Hello(Message):
fn __init__(inout self): ...
@staticmethod
fn default_message(): print("Hello World")
struct Bye(Message):
fn __init__(inout self): ...
@staticmethod
fn default_message(): print("Goodbye")
fn main():
Hello.default_message()
Bye.default_message()
8.3. Associated aliases
We saw how traits allow us to abstract over methods and even static methods. Another key feature of Mojo traits is that they even let us abstract over aliases. This means that structs that implement traits can override aliases and provide their own aliases. One benefit is when we do not want to prescribe what types the traits use within its methods, instead we want the implementors to provide the types. This makes API designs much more elegant and extensible.
trait Message:
@staticmethod
fn default_message(): ...
struct Hello(Message):
fn __init__(inout self): ...
@staticmethod
fn default_message(): print("Hello World")
struct Bye(Message):
fn __init__(inout self): ...
@staticmethod
fn default_message(): print("Goodbye")
fn main():
Hello.default_message()
Bye.default_message()