Traits
A trait is a set of requirements that a type must implement. You can think of it as a contract: a type that conforms to a trait guarantees that it implements all of the features of the trait.
Traits are similar to Java interfaces, C++ concepts, Swift protocols, and Rust traits. If you're familiar with any of those features, Mojo traits solve the same basic problem.
You've probably already seen some traits, like Copyable and Movable, used in
example code. This section describes how traits work, how to use existing
traits, and how to define your own traits.
Background
In dynamically-typed languages like Python, you don't need to explicitly declare that two classes are similar. This is easiest to show by example:
class Duck:
def quack(self):
print("Quack.")
class StealthCow:
def quack(self):
print("Moo!")
def make_it_quack(maybe_a_duck):
try:
maybe_a_duck.quack()
except:
print("Not a duck.")
make_it_quack(Duck())
make_it_quack(StealthCow())The Duck and StealthCow classes aren't related in any way, but they both
define a quack() method, so they work the same in the make_it_quack()
function. This works because Python uses dynamic dispatch—it identifies the
methods to call at runtime. So make_it_quack() doesn't care what types
you're passing it, only the fact that they implement the quack() method.
In a statically-typed environment, this approach doesn't work: Mojo functions require you to specify the type of each argument. If you wanted to write this example in Mojo without traits, you'd need to write a function overload for each input type.
@fieldwise_init
struct Duck(Copyable):
def quack(self):
print("Quack")
@fieldwise_init
struct StealthCow(Copyable):
def quack(self):
print("Moo!")
def make_it_quack(definitely_a_duck: Duck):
definitely_a_duck.quack()
def make_it_quack(not_a_duck: StealthCow):
not_a_duck.quack()
make_it_quack(Duck())
make_it_quack(StealthCow())Quack
Moo!This isn't too bad with only two types. But the more types you want to support, the less practical this approach is.
You might notice that the Mojo versions of make_it_quack() don't include the
try/except statement. We don't need it because Mojo's static type checking
ensures that you can only pass instances of Duck or StealthCow into the
make_it_quack() function.
Using traits
Traits solve this problem by letting you define a shared set of behaviors that
types can implement. Then you can write a function that depends on the trait,
rather than individual types. As an example, let's update the make_it_quack()
example using traits. This will involve three steps:
- Defining a new
Quackabletrait. - Adding the trait to the
DuckandStealthCowstructs. - Updating the
make_it_quack()function to depend on the trait.
Defining a trait
The first step is defining a trait that requires a quack() method:
trait Quackable:
def quack(self):
...A trait looks a lot like a struct, except it's introduced by the trait
keyword. Note that the quack() method signature is followed by three dots
(...). This indicates it isn't implemented within the trait. In this
example, quack is a required method and must be implemented by
any conforming struct.
A trait can supply a default implementation, so conforming structs
don't need to implement the method themselves. You can provide a full
implementation or use the pass keyword. Using pass creates a
no-op method that does nothing. In your conforming struct, you
can choose to override this default implementation:
trait Quackable:
def quack(self):
passFor more information, see Default method implementations.
A trait can also include comptime members—compile-time constant values that
must be defined by conforming structs. comptime members are useful for writing
traits that describe generic types. For more information, see
comptime members for generics.
Adding traits to structs
Next, we need some structs that conform to the Quackable trait. Since the
Duck and StealthCow structs above already implement the quack() method,
all we need to do is add the Quackable trait to the traits it conforms to
(in parenthesis, after the struct name).
(If you're familiar with Python, this looks just like Python's inheritance syntax.)
@fieldwise_init
struct Duck(Copyable, Quackable):
def quack(self):
print("Quack")
@fieldwise_init
struct StealthCow(Copyable, Quackable):
def quack(self):
print("Moo!")The struct needs to implement any methods that are declared in the trait. The compiler enforces conformance: if a struct says it conforms to a trait, it must implement everything required by the trait, or the code won't compile.
Using a trait as a type bound
Finally, you can define a function that takes a Quackable like this:
def make_it_quack[DuckType: Quackable](maybe_a_duck: DuckType):
maybe_a_duck.quack()Or using the shorthand form:
def make_it_quack2(maybe_a_duck: Some[Quackable]):
maybe_a_duck.quack()This syntax may look a little unfamiliar if you haven't dealt with Mojo
parameters before. What the first signature means is
that maybe_a_duck is an argument of type DuckType, where DuckType is a
type that must conform to the Quackable trait. Quackable is called the type
bound for DuckType.
The Some[Quackable] form expresses the same idea: the type of maybe_a_duck
is some concrete type that conforms to the trait Quackable.
Both forms work the same, except that the first form explicitly names the type value. This can be useful, for example, if you want to take two values of the same type:
def take_two_quackers[DuckType: Quackable](quacker1: DuckType, quacker2: DuckType):
passPutting it all together
Using the function is simple enough:
make_it_quack(Duck())
make_it_quack(StealthCow())Quack
Moo!Note that you don't need the square brackets when you call make_it_quack():
the compiler infers the type of the argument, and ensures the type has the
required trait.
One limitation of traits is that you can't add traits to existing types. For
example, if you define a new Numeric trait, you can't add it to the standard
library Float64 and Int types. However, the standard library already
includes quite a few traits, and we'll be adding more over time.
Traits can require static methods
In addition to regular instance methods, traits can specify required static methods.
trait HasStaticMethod:
@staticmethod
def do_stuff(): ...
def fun_with_traits[type: HasStaticMethod]():
type.do_stuff()Default method implementations
Often, some or all of the structs that conform to a given trait can use the same implementation for a given required method. In this case, the trait can include a default implementation. A conforming struct can still define its own version of the method, overriding the default implementation. But if the struct doesn't define its own version, it automatically inherits the default implementation.
Defining a default implementation for a trait looks the same as writing a method for a struct:
trait DefaultQuackable:
def quack(self):
print("Quack")
@fieldwise_init
struct DefaultDuck(Copyable, DefaultQuackable):
passWhen looking at the API doc for a standard library trait, you'll see methods that you must implement listed as required methods, and methods that have default implementations listed as provided methods.
The
Equatable
trait is a good example of the use case for default implementations. The trait
includes two methods: __eq__() (corresponding to the == operator) and
__ne__() (corresponding to the != operator). Every type that conforms to
Equatable needs to define the __eq__() method for itself, but the
trait supplies a default implementation for __ne__(). Given an __eq__()
method, the definition of __ne__() is trivial for most types:
def __ne__(self, other: Self) -> Bool:
return not self.__eq__(other)Trait compositions
You can compose traits using the & sigil. This lets you define new traits
that are simple combinations of other traits. You can use a trait composition
anywhere that you'd use a single trait:
trait Flyable:
def fly(self): ...
def quack_and_go[type: Quackable & Flyable](quacker: type):
quacker.quack()
quacker.fly()
@fieldwise_init
struct FlyingDuck(Copyable, Quackable, Flyable):
def quack(self):
print("Quack")
def fly(self):
print("Whoosh!")You can also use the comptime keyword to create a shorthand name for a
trait composition:
comptime DuckLike = Quackable & Flyable
struct ToyDuck(DuckLike):
# ... implementation omittedYou can also compose traits using refinement, by defining a new, empty trait like this:
trait DuckTrait(Quackable, Flyable):
passHowever, this is less flexible than using a trait composition and not
recommended. The difference is that using the trait keyword defines a new,
named trait. For a struct to conform to this trait, you need to explicitly
include it in the struct's signature. On the other hand, the DuckLike
comptime value represents a composition of two separate traits, Quackable
and Flyable, and anything that conforms to those two traits conforms to
DuckLike. For example, consider the FlyingDuck type shown above:
struct FlyingDuck(Copyable, Quackable, Flyable):
# ... etcBecause FlyingDuck conforms to both Quackable and Flyable, it also
conforms to the DuckLike trait composition. But it doesn't
conform to DuckTrait, since it doesn't include DuckTrait in its list of
traits.
Trait refinement
Traits refine other traits. A trait that refines another trait includes all of the requirements declared by the original trait. For example:
trait Animal:
def make_sound(self):
...
# Bird refines Animal
trait Bird(Animal):
def fly(self):
...Since Bird refines Animal, a struct that conforms to the Bird trait
must implement both make_sound() and fly(). And since every Bird
conforms to Animal, a struct that conforms to Bird can be passed to any
function that requires an Animal.
You can define a trait as a refinement of multiple traits by listing them in parentheses. This can be either a comma-separated list of traits or a trait composition. For example, you might define a NamedAnimal trait that combines the requirements of the Animal trait and a new Named trait:
trait Named:
def get_name(self) -> String:
...
trait NamedAnimal(Animal, Named):
def emit_name_and_sound(self):
...Refinement is useful when you're creating a new trait that adds additional requirements. If you simply want to express the union of two or more traits, use a simple trait composition instead:
comptime NamedAnimal = Animal & NamedTraits and lifecycle methods
Traits can specify required lifecycle methods, including constructors, copy constructors and move constructors.
For example, the following code creates a MassProducible trait. A
MassProducible type has a default (no-argument) constructor and can be moved.
It uses two built-in traits:
Defaultable, which requires a default
(no-argument) constructor, and
Movable,
which requires the type to have a move
constructor.
The factory[]() function returns a newly-constructed instance of a
MassProducible type. The following example shows the definitions of
the Defaultable and Movable traits in comments for reference:
# trait Defaultable
# def __init__(out self): ...
# trait Movable
# def __init__(out self, *, deinit take: Self): ...
comptime MassProducible = Defaultable & Movable
def factory[type: MassProducible]() -> type:
return type()
struct Thing(MassProducible):
var id: Int
def __init__(out self):
self.id = 0
def __init__(out self, *, deinit take: Self):
self.id = take.id
var thing = factory[Thing]()Register passable types and the RegisterPassable trait
"Register passable" tells Mojo that a type should be passed in machine registers such as a CPU register. This means the type is always passed by value. For data types like an integer or floating-point number, this is much more efficient than storing values in stack memory.
Mojo supports two forms of register passable types.
The standard register passable type has the following capabilities and restrictions:
- They have normal lifecycles and can implement or override lifecycle methods.
- Every field must either be register passable or trivially register passable.
- The type must be
Movable. - Their self pointer is neither stable nor predictable. They can move in memory at any time so they aren't suitable for types that rely on pointer identity.
- When used as arguments or results, they can be exposed directly to C and C++ through foreign function interfaces (FFI) and don't need to be passed by pointer.
- They can't override the default move constructor. This guarantees moves are always side-effect-free.
You can create custom register-passable types by conforming a type
to the RegisterPassable trait
Trivial register types are typically data types. They include:
- Arithmetic types. This includes types such as
Int,Int32,Bool,Float64etc. - Pointers. The address value is trivial, not the data being pointed to.
- Arrays of other trivial types.
SIMDis a good example.
These types are provided by Mojo's standard library.
A trivially register passable type has the following capabilities and restrictions:
- They do not use lifecycle methods.
- Every field must be trivially register passable.
- The type must be
Copyable. - They use a special trivial (no-op) destructor. They are trivially
movable (via
Copyable, which refinesMovable), trivially copyable, and trivially destructible.
Built-in traits
The Mojo standard library includes many traits. They're implemented by a number of standard library types, and you can also implement these on your own types. These standard library traits include:
AbsableAnyTypeBoolableComparableCopyableDefaultableHashableImplicitlyDestructibleIndexerIntableIntableRaisingKeyElementMovablePathLikePowableSizedRoundableWritableWriter
The API reference docs linked above include usage examples for each trait. The following sections discuss a few of these traits.
The Sized trait
The Sized trait identifies types that
have a measurable length, like strings and arrays.
Specifically, Sized requires a type to implement the __len__() method.
This trait is used by the built-in len()
function. For example, if you're writing a custom list type, you could
implement this trait so your type works with len():
struct MyList(Copyable, Sized):
var size: Int
# ...
def __init__(out self):
self.size = 0
def __len__(self) -> Int:
return self.size
print(len(MyList()))0The Intable and IntableRaising traits
The Intable trait identifies a type that
can be converted to Int. The
IntableRaising trait describes a
type can be converted to an Int, but the conversion might raise an error.
Both of these traits require the type to implement the __int__() method. For
example:
@fieldwise_init
struct IntLike(Intable):
var i: Int
def __int__(self) -> Int:
return self.i
value = IntLike(42)
print(Int(value) == 42)TrueThe Writable trait
The Writable trait describes a type that can
produce a human-readable text representation by writing to a
Writer object. The
print() function requires that its arguments
conform to the Writable trait. This enables efficient stream-based writing by
default, avoiding unnecessary intermediate String heap allocations.
The Writable trait requires a type to implement a
write_to() method. This method
receives a mut writer: Some[Writer] argument. A generic
Writer that represents anything that can be
written to, such as a String buffer, a file, or a network stream. You invoke
the Writer instance's
write() method to write a
sequence of Writable arguments constituting the string representation of
your type.
An important feature of the Writer trait is that it only accepts valid UTF-8
text. This means that when you write data to a Writer, you can only use
StringSlice values (via the
write_string() method) or other
Writable types; you can't write arbitrary bytes. This guarantee ensures that
types like String can safely implement the Writer trait without risking
corruption of their internal UTF-8 data.
Writable also provides a
write_repr_to() method for
producing the "official" string representation of a type—the kind you would see
when calling repr() or using the {!r}
format specifier. If at all possible, this should look like a valid Mojo
expression that could be used to recreate a struct instance with the same value.
write_repr_to() has a default implementation that uses reflection,
so you only need to override it if your type needs a distinct debug
representation.
Here is a simple example of a type that implements Writable with a custom
write_repr_to():
@fieldwise_init
struct Dog(Copyable, Writable):
var name: String
var age: Int
# Allows the type to be written into any `Writer`
def write_to(self, mut writer: Some[Writer]):
t"Dog({self.name}, {self.age})".write_to(writer)
# Official representation when calling `repr`
def write_repr_to(self, mut writer: Some[Writer]):
t"Dog(name={self.name}, age={self.age})".write_to(writer)
dog = Dog("Rex", 5)
print(repr(dog))
print(dog)Dog(name=Rex, age=5)
Dog(Rex, 5)Lifetime management traits
Mojo provides two core traits for managing value lifetimes:
-
AnyType: The base trait that all types extend. Types conforming only toAnyTypemay require explicit destruction. -
ImplicitlyDestructible: Types that can be automatically destroyed by calling__del__()when their lifetime ends.
For detailed information about value destruction, explicit destruction with the
@explicit_destroy decorator, and when to use each approach, see Death of a
value.
Generic structs with traits
You can also use traits when defining a generic container. A generic container is a container (for example, an array or hashmap) that can hold different data types. In a dynamic language like Python it's easy to add different types of items to a container. But in a statically-typed environment, the compiler needs to be able to identify the types at compile time. For example, if the container needs to copy a value, the compiler needs to verify that the type can be copied.
The List type is an example of a
generic container. A single List can only hold a single type of data.
The list elements must conform to the Copyable trait:
struct List[T: Copyable]:For example, you can create a list of integer values like this:
var list: List[Int]
list = [1, 2, 3, 4]
for i in range(len(list)):
print(list[i], end=" ")1 2 3 4You can use traits to define requirements for elements that are stored in a
container. For example, List requires elements that can be moved and
copied. To store a struct in a List, the struct needs to conform to
the Copyable trait, which requires a
copy constructor and a
move constructor.
Building generic containers is an advanced topic. For an introduction, see the section on parameterized structs.
comptime members for generics
In addition to methods, a trait can include comptime members, which must be
defined by any conforming struct. For example:
trait Repeater:
comptime count: IntAn implementing struct must define a concrete constant value for the comptime
member, using any compile-time parameter value. For example, it can use a literal
constant or a compile-time expression, including one that uses the struct's
parameters.
struct Doublespeak(Repeater):
comptime count: Int = 2
struct Multispeak[verbosity: Int](Repeater):
comptime count: Int = Self.verbosity * 2 + 1The Doublespeak struct has a constant value for count, but the Multispeak
struct lets the user set the value using a parameter:
repeater = Multispeak[12]()Note that the field is named count, and the Multispeak parameter is named
verbosity. Parameters and comptime members are in the same namespace, so the
parameter can't have the same name as the comptime member.
comptime members are most useful for writing traits for generic types. For
example, imagine that you want to write a trait that describes a generic stack
data structure that stores elements that conform to the Copyable
trait.
By adding the element type as a comptime member on the trait, you can specify
generic methods on the trait:
trait Stacklike:
comptime EltType: Copyable
def push(mut self, var item: Self.EltType):
...
def pop(mut self) -> Self.EltType:
...The following struct implements the Stacklike trait using a List as the
underlying storage:
struct MyStack[type: Copyable & ImplicitlyDestructible](Stacklike):
"""A simple Stack built using a List."""
comptime EltType = Self.type
comptime list_type = List[Self.EltType]
var list: Self.list_type
def __init__(out self):
self.list = Self.list_type()
def push(mut self, var item: Self.EltType):
self.list.append(item^)
def pop(mut self) -> Self.EltType:
return self.list.pop()
def dump[
WritableEltType: Writable & Copyable
](self: MyStack[WritableEltType]):
print("[", end="")
for item in self.list:
print(item, end=", ")
print("]")The MyStack type adds a dump() method that prints the contents of the stack.
Because a struct that conforms to Copyable is not necessarily
printable, MyStack uses
conditional conformance to
define a dump() method that works as long as the element type is
writable.
The following code exercises this new trait by defining a generic method,
add_to_stack() that adds an item to any Stacklike type.
def add_to_stack[S: Stacklike](mut stack: S, var item: S.EltType) raises:
stack.push(item^)
def main() raises:
s = MyStack[Int]()
add_to_stack(s, 12)
add_to_stack(s, 33)
s.dump() # [12, 33, ]
print(s.pop()) # 33Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!