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.

Trait

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()

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()