#set document(title: "2.8 Mojo structs", author: "Modular Inc. / XYZ Homework") #set page(width: 8.5in, height: auto, margin: 1in) #import "@preview/cetz:0.5.2" #set text(font: ("STIX Two Text", "Libertinus Serif", "New Computer Modern"), size: 10.5pt, lang: "en") #show math.equation: set text(font: ("STIX Two Math", "New Computer Modern Math")) #set par(justify: true, leading: 0.62em, spacing: 0.9em) #set enum(spacing: 1.1em) // room between list items so tall inline fractions don't collide #set list(spacing: 1.1em) #set table(stroke: 0.5pt + rgb("#c7ccd3")) #let BLUE = rgb("#183B6F") // brand navy — section bars + example/solution labels (white on navy 11.09:1) #let ORANGE = rgb("#A94509") // brand primary-700 — AA-safe deep orange for TEXT (5.93:1 on white; raw brand #F37021 is 2.94:1 and must never carry text) #let RED = rgb("#DC2626") // brand error-600 #let GREEN = rgb("#059669") // brand success-600 (decoration only; small green text uses green-text #007942) #show heading.where(level: 1): it => block(width: 100%, above: 0pt, below: 16pt, fill: gradient.linear(BLUE, rgb("#2C5AA0")), inset: (x: 14pt, y: 12pt), radius: 3pt, text(fill: white, weight: "bold", size: 19pt, it.body)) #show heading.where(level: 2): it => block(width: 100%, above: 18pt, below: 10pt, fill: BLUE, inset: (x: 10pt, y: 6pt), radius: 2pt, text(fill: white, weight: "bold", size: 12pt, it.body)) #show heading.where(level: 3): it => text(fill: ORANGE, weight: "bold", size: 12.5pt, it.body) #show heading.where(level: 4): it => text(fill: BLUE, weight: "bold", size: 10.5pt, it.body) #let examplebox(label, title, body) = block(width: 100%, breakable: true, fill: rgb("#EFF1F5"), stroke: 0.5pt + rgb("#CFDDF0"), radius: 4pt, inset: 10pt, above: 12pt, below: 12pt)[ #block(below: 6pt)[#box(fill: BLUE, inset: (x: 6pt, y: 2pt), radius: 2pt, text(fill: white, weight: "bold", size: 8.5pt, label)) #h(0.4em) #strong[#title]] #body] // rail = decorative left rule (raw brand token); labelcolor = AA-safe label text shade #let notebox(label, rail, labelcolor, tint, body) = block(width: 100%, breakable: true, fill: tint, stroke: (left: 3pt + rail), inset: (left: 10pt, rest: 8pt), radius: (right: 4pt), above: 11pt, below: 11pt)[ #text(fill: labelcolor, weight: "bold", size: 7.5pt, tracking: 0.5pt)[#upper(label)] #linebreak() #body] #let solutionbox(body) = block(above: 4pt, below: 8pt)[ #text(fill: BLUE, weight: "bold", size: 8.5pt)[Solution] #linebreak() #body] #let figph(msg) = block(width: 100%, height: 60pt, fill: rgb("#f6f7f9"), stroke: (paint: rgb("#c7ccd3"), dash: "dashed"), radius: 4pt, inset: 10pt)[ #align(center + horizon, text(fill: rgb("#889"), style: "italic", size: 9pt, msg))] // Standardize inlined figure sizes: measure the natural CeTZ canvas, then scale to a // consistent envelope (aspect-aware; see build_typst.py FIG_* constants). Unlike the // print preamble, dimensions are FLOORED: in an editor a user can trim a figure to a // degenerate 1-D shape (a bare line), and w/h or tw/w would then divide by zero. #let _STD_W = 3.5 #let _WIDE_W = 5.6 #let _MAX_H = 3.4 #let _ASPECT_WIDE = 2.2 #let _UPSCALE_MAX = 1.15 #let stdfig(body) = context { let m = measure(body) let w = calc.max(m.width / 1in, 0.01) let h = calc.max(m.height / 1in, 0.01) let tw = if w / h > _ASPECT_WIDE { _WIDE_W } else { _STD_W } let s = calc.min(tw / w, _MAX_H / h, _UPSCALE_MAX) align(center, box(scale(x: s * 100%, y: s * 100%, reflow: true, body))) } #show figure: set block(breakable: false) #set figure(gap: 8pt) #show figure.caption: set text(size: 8.5pt, fill: rgb("#555")) == 2.8#h(0.6em)Mojo structs A struct is Mojo's primary way to define your own type. When you want to model both data and behavior—whether that's a small value type, a numeric abstraction, or the foundation of a larger system—you'll use a struct. At a high level, a Mojo struct lets you bundle data together with the operations that act on that data. This makes structs a natural way to represent concepts in your program, rather than passing loosely related values through functions. Each Mojo struct is a data structure that lets you encapsulate #emph[fields] and #emph[methods] to store and operate on data. Structs can define the following members: - #strong[Fields] are variables that store data relevant to the struct. - #strong[Methods] are functions defined in a struct that normally act upon the field data. - #strong[Static methods] are functions provided by the type to perform behaviors, provide constants, or create specialized instances. - #strong[Dunder methods] are named for their \_d\_ouble #emph[under]-scored form, with \_\_ on both sides. Also called "special methods", they help define behaviors such as initialization and allow structs to conform to traits. - #strong[comptime members] enable compile-time references that can be used for optimization. For example, if you're building a graphics program, you can use a struct to define an Image that has fields to store information about each image (such as its component pixels) and methods that perform actions on it (such as rotating the image). Mojo's struct format is designed to provide a static, memory-safe data structure that's both powerful and performant. Unlike dynamic objects (such as Python classes) that can be modified freely at runtime, structs are defined at compile time, which allows Mojo to generate highly optimized code. All struct fields must be declared using var and include a type annotation. This requirement is part of Mojo's compile-time guarantees, helping ensure both performance and memory safety. === Struct definition You can define a simple struct called MyPair with two fields like this: struct MyPair: var first: Int var second: Int However, you can't instantiate this struct because it has no constructor method. So here it is with a constructor to initialize the two fields: struct MyPair: var first: Int var second: Int def \_\_init\_\_(out self, first: Int, second: Int): self.first = first self.second = second Notice that the first argument in the \_\_init\_\_() method is out self. You'll have a self argument as the first argument on all struct methods. It references the current struct instance (it allows code in the method to refer to "itself"). #emph[When you call the constructor, you never pass a value for self—Mojo passes it in automatically.] The out portion of out self is an argument convention that declares self as a mutable reference that starts out as uninitialized and must be initialized before the function returns. Many types use a field-wise constructor like the one shown for MyPair above: it takes an argument for each field, and initializes the fields directly from the arguments. To save typing, Mojo provides a #link("https://mojolang.org/docs/reference/decorators/fieldwise-init/")[\@fieldwise\_init] decorator, which generates a field-wise constructor for the struct. So you can rewrite the MyPair example above like this: \@fieldwise\_init struct MyPair: var first: Int var second: Int The \_\_init\_\_() method is one of many special methods (also known as "dunder methods" because they have #emph[d]ouble #emph[under]scores) with pre-determined names. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ You can't assign values when you declare fields. You must initialize all of the struct's fields in the constructor. (If you try to leave a field uninitialized, the code won't compile.) ] === Constructing a struct type Once you have a constructor, with \_\_init\_\_ or using \@fieldwise\_init, you can create an instance of MyPair and set the fields: var mine = MyPair(2, 4) print(mine.first) 2 #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Initializer lists] Mojo initializer lists let you construct instances without spelling out the full type name and parameters. If the full type can be inferred from context, pass the constructor arguments directly between braces, with or without keywords. For example {0.5, fish="salmon"} calls \_\_init\_\_(0.5, fish="salmon") on the appropriate struct type. This is equivalent to MyStruct(0.5, fish="salmon") if the type is inferred to be MyStruct. ] === Mutating a struct By default, a struct's methods receive an immutable self, so they can't modify the struct's fields. For example: struct MyStruct: var value: Int def increment(self): self.value += 1 \# ERROR: expression must be mutable in assignment \# ... To allow a method to mutate the instance, declare its receiver as mut self. This makes self mutable inside the method and allows changes to its fields that persist after the method returns: struct MyStruct: var value: Int def increment(mut self): self.value += 1 \# Works: Mutable \`self\` allows assignment \#... #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Read more about mutable arguments and the mut keyword: Mutable arguments (mut) ] === Making a struct Copyable Mojo structs are not copyable or movable by default. For example, the following code produces errors: var a = MyPair(1, 2) \# Implicit copy var b = a \# value of type 'MyPair' cannot be implicitly copied, \# it does not conform to 'ImplicitlyCopyable' \# Explicit copy var c = a.copy() \# value of type 'MyPair' cannot be implicitly copied, \# it does not conform to 'ImplicitlyCopyable' \# Move var d = a^ \# value of type 'MyPair' cannot be copied or moved; consider \# conforming it to 'Copyable', which also adds 'Movable' \# conformance. In most cases, you can make a struct copyable (and movable) just by adding the Copyable trait. ==== Movability To make a struct move-only, add the Movable trait: struct MyPair(Movable): ... Mojo will generate a move constructor for you. You rarely need to write your own custom move constructor. For more information, see the section on move constructors. ==== Copyability To make a struct copyable, add the Copyable trait: struct MyPair(Copyable): ... In most cases, that's all you need to do. Mojo will generate a copy constructor (\_\_init\_\_(out self, \*, copy: Self) method) for you. You don't need to write your own unless you need custom logic in the copy constructor; for example, if your struct dynamically allocates memory. For more information, see the section on copy constructors. The Copyable trait provides two ways to copy a value: the copy() instance method and the copy initializer. In addition, Copyable automatically adds movability, so you won't need to add both Copyable #emph[and] Movable to your declarations. ==== Implicit copyability To make a struct implicitly copyable, add the ImplicitlyCopyable trait: struct MyPair(ImplicitlyCopyable): ... ImplicitlyCopyable automatically implies Copyable and Movable, so all the notes related to copyability apply here. A type should only be implicitly copyable if copying the type is inexpensive and has no side effects. Unnecessary copies can be a big drain on memory and performance, so use this trait with caution. === Fields Fields store a struct's data. When you declare a field, it becomes part of the struct's memory layout. Because the compiler knows every field's type at compile time, it can: - Calculate the struct's exact memory footprint - Ensure all fields are initialized before use - Generate fast, direct access to field data - Prevent changes to the struct's layout at runtime Fields share the lifetime of their struct instance. They are created when the struct is created and destroyed when the struct is destroyed. This model avoids dangling references and partially constructed objects. Outside of your struct implementation, you access fields with dot notation (my\_struct.field\_name). Within the struct, your methods access fields using self (self.field\_name). Mojo knows each field's location at compile time, making field access direct and efficient. ==== Field requirements #strong[You must] declare field members with var in structs: struct MyStruct: value: Int \# Error. Missing \`var\` keyword var count: Int \# Yes Unlike local variables in functions, this requirement lets Mojo reason about a struct's layout and guarantees that its memory is safe and predictable. #strong[You must] use unique symbols for fields, methods, or comptime members. These all exist in the same namespace: struct MyStruct: var count: Int var count: String \# Error. Invalid redeclaration of \`count\` #strong[You can] re-use a struct member's name for an argument or method variable. struct MyStruct: var foo: Int def use\_argument(self, foo: Int): \# Argument shadows field print(foo) \# Prints argument value def use\_local(self, value: Int): var foo = value \# Local variable shadows field print(foo, self.foo) \# Prints local, then field #strong[You must] mark self as mutable if updating a field value. struct MyStruct: var foo: Int def update\_foo(mut self, new\_value: Int): self.foo = new\_value #strong[You must] initialize fields within constructors, and not at the point of declaration. struct MyStruct: var foo: Int = 10 \# Error: Unknown tokens comptime bar = 10 \# Yes comptime members are compile-time constants (not fields) and don't occupy instance storage, so they can be initialized at the point of declaration. ==== Field conventions Like other Mojo elements, fields normally adhere to #link("https://github.com/modular/modular/blob/mojo/v1.0.0b2/mojo/stdlib/docs/style-guide.md#code-conventions")[certain conventions]: You should use conventional naming for field members: - Prefer lowercase snake\_case for field names (for example, user\_count, max\_capacity). - Use descriptive names that indicate purpose, and not their type (for example, error\_msg not msg\_string). - For members meant for internal use or to maintain invariants, add an underscore prefix (for example, \_private\_field). - For boolean fields, use is\_ or has\_ prefixes (for example, is\_valid, has\_data). - Avoid single-letter names except for common mathematical conventions (such as x, y, z for coordinates). === Methods In addition to special methods like \_\_init\_\_(), you can add any other method you want to your struct. For example: \@fieldwise\_init struct MyPair: var first: Int var second: Int def get\_sum(self) -\> Int: return self.first + self.second var mine = MyPair(6, 8) print(mine.get\_sum()) 14 Notice that get\_sum() also uses the self argument, because this is the only way you can access the struct's fields in a method. The name self is just a convention, and you can use any name you want to refer to the struct instance that is always passed as the first argument. Methods that take the implicit self argument are called #emph[instance methods] because they act on an instance of the struct. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ The self argument in a struct method is the only argument in a def function that does not require a type. You can include the type if you want, but you can elide it because Mojo already knows its type (MyPair in this case). ] ==== Static methods A struct can have #emph[static methods]. A static method can be called without creating an instance of the struct. Unlike instance methods, a static method doesn't receive the implicit self argument, so it can't access any fields on the struct. To declare a static method, use the \@staticmethod decorator and don't include a self argument: struct Logger: def \_\_init\_\_(out self): pass \@staticmethod def log\_info(message: String): print("Info: ", message) You can invoke a static method by calling it on the type (in this case, Logger). You can also call it on an instance of the type. Both forms are shown below: Logger.log\_info("Static method called.") var l = Logger() l.log\_info("Static method called from instance.") Info: Static method called. Info: Static method called from instance. === Structs compared to classes If you're familiar with other object-oriented languages, then structs might sound a lot like classes, and there are some similarities, but also some important differences. Eventually, Mojo will also support classes to match the behavior of Python classes. So, let's compare Mojo structs to Python classes. They both support methods, fields, operator overloading, decorators for metaprogramming, and more, but their key differences are as follows: - Python classes are dynamic: they allow for dynamic dispatch, monkey-patching (or "swizzling"), and dynamically binding instance fields at runtime. - Mojo structs are static: they are bound at compile-time (you cannot add methods at runtime). Structs allow you to trade flexibility for performance while being safe and easy to use. - Mojo structs do not support inheritance ("sub-classing"), but a struct can implement traits. - Python classes support class attributes—values that are shared by all instances of the class, equivalent to class variables or static data members in other languages. - Mojo structs don't support static data members. Syntactically, the biggest difference compared to a Python class is that all fields in a struct must be explicitly declared with var. In Mojo, the structure and contents of a struct are set at compile time and can't be changed while the program is running. Unlike in Python, where you can add, remove, or change attributes of an object on the fly, Mojo doesn't allow that for structs. However, the static nature of structs helps Mojo run your code faster. The program knows exactly where to find the struct's information and how to use it without any extra steps or delays at runtime. Mojo's structs also work really well with features you might already know from Python, like operator overloading (which lets you change how math symbols like + and - work with your own data, using special methods). As mentioned above, all Mojo's standard types (Int, String, etc.) are made using structs, rather than being hardwired into the language itself. This gives you more flexibility and control when writing your code, and it means you can define your own types with all the same capabilities (there's no special treatment for the standard library types). === Special methods Special methods (or "dunder methods") such as \_\_init\_\_() are pre-determined method names that you can define in a struct to perform a special task. Although it's possible to call special methods with their method names, the point is that you never should, because Mojo automatically invokes them in circumstances where they're needed (which is why they're also called "magic methods"). For example, Mojo calls the \_\_init\_\_() method when you create an instance of the struct; and when Mojo destroys the instance, it calls the \_\_del\_\_() method (if it exists). Even operator behaviors that appear built-in (+, \<, ==, |, and so on) are implemented as special methods that Mojo implicitly calls upon to perform operations or comparisons on the type that the operator is applied to. Mojo supports a long list of special methods; far too many to discuss here, but they generally match all of #link("https://docs.python.org/3/reference/datamodel#special-method-names")[Python's special methods] and they usually accomplish one of two types of tasks: - Operator overloading: A lot of special methods are designed to overload operators such as \< (less-than), + (add), and | (or) so they work appropriately with each type. For more information, see Implement operators for custom types. - Lifecycle event handling: These special methods deal with the lifecycle and value ownership of an instance. For example, \_\_init\_\_() and \_\_del\_\_() demarcate the beginning and end of an instance lifetime, and other special methods define the behavior for other lifecycle events such as how to copy or move a value. You can learn all about the lifecycle special methods in the Value lifecycle section. However, most structs are simple aggregations of other types, so unless your type requires custom behaviors when an instance is created, copied, moved, or destroyed, you can synthesize the essential lifecycle methods you need (and save yourself some time) using the \@fieldwise\_init decorator (described in Struct definition), and the Copyable and Movable traits (described in Making a struct copyable).