7. Struct

In the previous chapter we saw the different data types supported by Mojo out of the box. But what if you wanted to implement your own data type? Mojo provides struct keyword for that purpose. The term "struct" was popularized by the ALGOL family of languages and is a short form for the term structure. In Mojo, struct allows one to group related values together as a single unit. Members variables of a struct must have type annotation.

struct Person:
    var first_name: String
    var last_name: String

    fn __init__(inout self):
        self.first_name = "Mickey"
        self.last_name = "Mouse"

    fn get_full_name(self) -> String:
        return self.first_name + " " + self.last_name

The code shown before shows how a struct is defined within Mojo. You start with keyword struct and then give a name for the struct. Then you can define the member variables of the struct. Here we defined first_name and last_name strings. You can also define functions within a struct. Functions defined inside the body of a struct are known as "method". The body of the struct is indented with whitespace.

You may have noticed that we have defined a method init. This is the initializer or in other languages known as the constructor. In order for a struct to be used in a program, we need to define a method that initializes the struct. The main responsibility of the initializer is to setup the struct in a valid state and to ensure that all the member variables also have a valid state. If we omit the initializer, the compiler would complain that init is missing.

In the init method, the first argument is mandatory and it is named self by convention. It does not really matter whether you call the first argument self or some other name as Mojo would accept any other name. However, Mojo adopts the same convention as Python and calls the first argument self. It is highly recommended to follow that convention as it makes the code easier to read and understand by other programmers. The first argument also has a keyword inout. For init methods, it is mandatory to have inout keyword in front of the first self argument. It indicates to Mojo that the self is mutable reference. We will cover this later on in this book.

To refer to the member variables of a strut, we need to prefix the variable with self.. Mojo allows new variables to be defined with the same names if the scope of the variable is different. The prefix self. makes it possible for the Mojo compiler to determine that the struct’s member variable is being referred to and not to another variable of the same name in the function scope.

The anatomy of a struct is shown in the following diagram.

Struct

Syntax to instantiate a struct is quite similar to a function call. In the following code listing an instance of Person is stored in the variable client.

    var client: Person = Person()
    print(client.get_full_name())

You can define initializers with additional arguments. You can also define more than one initializers. The initializers can be given arguments similar to how arguments are passed to a function.

struct Person:
    var first_name: String
    var last_name: String

    fn __init__(inout self):
        self.first_name = "Mickey"
        self.last_name = "Mouse"

    fn __init__(inout self, fname: String, lname: String): # Second initializer
        self.first_name = fname
        self.last_name = lname

    fn get_full_name(self) -> String: # Instance method
        return self.first_name + " " + self.last_name

fn main():
    var client: Person = Person("Donald", "Duck") # Instantiating Person
    print(client.get_full_name()) # Calling an instance method

7.1. Instance methods

As mentioned earlier, a struct can define methods within it. There are two types of methods. One is instance method and the other is static method. Instance methods are called on an instance of the struct. In the previous code listing, get_full_name is an instance method because it the first argument self which is the instance of the struct. It uses self to refer to the instance variables of the struct, for example self.first_name.

To call the instance method, we used the syntax client.get_full_name(). Note that even though get_full_name had an argument self passed to it, we do not pass that argument to get_full_name when we call it. What is happening here? One way to look at it is that when we call client.get_full_name(), behind the scene the compiler passes client as the first argument to get_full_name. This syntax is quite popular in many object oriented languages, and since Python has this syntax, Mojo also took it over.

7.2. Static methods

What if we do not have to refer to instance variables or even other instance methods in our method, but still want to have the method scoped within the struct? In this case Mojo offers static methods. Static methods are very similar to functions and they are within the scope of the struct, but not bound to a particular instance of the struct. Mojo compiler can perform some optimizations to make static method invocations much faster than instance methods.

struct Vehicle:
    var model_name: String

    fn __init__(inout self, model_name: String):
        self.model_name = model_name

    fn get_model(self) -> String: 
        return self.model_name
    
    @staticmethod
    fn get_default_model() -> String: 
        return "VW"

fn main():
    var v: Vehicle = Vehicle("Mercedes")
    print(v.get_model()) # Call instance method
    print(Vehicle.get_default_model()) # Call static method

    print(v.get_default_model()) # Possible, but not a good style to call static method.
    print(Vehicle.get_model(v)) # Also possible, but not a good style to call an instance method.

In the previous code listing, get_default_model was defined using @staticmethod decorator. We will cover decorators in detail in a later chapter. The @staticmethod on a method indicates to the Mojo compiler that this method should be a static method.

Static methods are called using the name of the struct itself, instead of the name of the variable that contains the struct’s instance. For example, in the code listing the static method was called by referring to the Vehicle struct directly as in Vehicle.get_default_model().

It is possible to call static methods through an instance of the struct, but that style is discouraged because for a person reading the code, it is confusing.

7.3. Implicit conversion

You may have noticed that assignment var x: String = "A string literal" ` works, even though we saw earlier that anything within the double quotes `"" is of type StringLiteral. The above assignment works because Mojo has support for implicit conversions.

Mojo has a very simple approach for implicit conversions. Suppose that struct A has an initializer that takes an argument with type of StringLiteral. Then when we assign a StringLiteral to a variable of type A, it implicitly calls that initializer, resulting in initialization of the variable with an instance of A with that given string literal passed as an argument.

The following examples makes it more clear.

struct Vehicle:
    var model_name: String

    fn __init__(inout self, model_name: StringLiteral):
        self.model_name = model_name

    fn get_model(self) -> String: 
        return self.model_name
    
fn main():
    var v: Vehicle = "Ford"
    print(v.get_model())