#set document(title: "4.2 Deep dive - Instance initialization", 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")) == 4.2#h(0.6em)Deep dive - Instance initialization Mojo tracks two kinds of initialization for structs: #emph[fieldwise] and #emph[logical]. Fieldwise initialization means the struct's storage holds valid values for each field. Logical initialization means the instance as a whole is in a valid, usable state. Understanding the difference between these two forms of initialization is essential for working with structs correctly. === The basics You create struct instances by calling the \_\_init\_\_() constructor: struct Person: var name: String var age: Int def \_\_init\_\_(out self, name: String, age: Int): self.name = name self.age = age def main(): var me = Person("Alice", 30) Calling Person("Alice", 30) is syntactic sugar for calling the constructor directly: var me: Person me = Person.\_\_init\_\_("Alice", 30) \# Identical When constructing a Person, the compiler first allocates stack space for the instance me, then \_\_init\_\_() initializes that space. === Fieldwise vs logical initialization Initializing a struct by assigning values directly to its fields may populate the data, but it does not make the instance usable: struct Person(Writable): var age: Int var height: Int ... def main(): var me: Person me.age = 25 me.height = 6 print(me) \# Error \# error: 'me' used with all fields manually initialized \# but without calling an '\_\_init\_\_' method /var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-8g9tkr79/ch04-001.mojo:11:10: error: 'me' used with all fields manually initialized but without calling an '\_\_init\_\_' method print(me) \# Error ^ /var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-8g9tkr79/ch04-001.mojo:8:9: note: 'me' declared here var me: Person ^ /Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo: error: failed to run the pass manager In this example, all fields contain valid values, but the instance is still not considered initialized. Mojo tracks two distinct initialization states: - Fieldwise initialization means each field contains valid data and the instance's storage has been populated. - Logical initialization means the instance as a whole is valid and its \_\_init\_\_() method has run. Fieldwise initialization alone isn't enough. The compiler requires logical initialization before an instance can be used: var me = Person(25, 6) \# Now OK: \_\_init\_\_() ran print(me) Calling \_\_init\_\_() makes an instance #emph[logically] initialized. This process isn't just about assigning data. It ensures that any required initialization logic ran and that the instance is safe to use. === Inside \_\_init\_\_() Within \_\_init\_\_(), the incoming self instance is considered logically initialized, but its fields are still uninitialized: def \_\_init\_\_(out self, name: String, age: Int): \# At this point: \# - Logically initialized (self is valid as an instance) \# - Fieldwise uninitialized (fields have no values yet) self.name = name self.age = age \# Now both logically and fieldwise initialized This distinction is intentional. Entering \_\_init\_\_() establishes the instance itself, but it's your responsibility to populate its fields. You must initialize #emph[every field] in \_\_init\_\_(): def \_\_init\_\_(out self, name: String, age: Int): self.name = name \# Error: field 'age' not initialized in \_\_init\_\_ The \_\_init\_\_() signature doesn't have to mirror the struct's fields. You can use parameters, constants, or external values to initialize those fields: \# Parameters can be used to initialize fields self.\_store = List\[T\](capacity=Count) \# Constants can be used to initialize fields self.string = "" \# External values can be used to initialize fields from std.math import pi self.default\_angle = pi / 2.0 self.uuid = MyUUIDImplementation.uuid() You can't call methods until all fields are initialized: def \_\_init\_\_(out self, name: String): self.greet() \# Error: self not fully initialized self.name = name self.greet() \# OK: all fields initialized Field initialization is restricted to \_\_init\_\_() methods. Regular functions can't #emph[initialize] individual fields of an out argument, but \_\_init\_\_() methods can. === Moving from fields Moving a value out of a field #emph[deinitializes that field]: struct Person(Writable): var name: String var age: Int def \_\_init\_\_(out self, name: String, age: Int): self.name = name self.age = age def main(): var me = Person("Connor", 25) var name\_owner = me.name^ \# Move value from me.name. print(me) \# Error: use of uninitialized value 'me' /var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-8g9tkr79/ch04-002.mojo:12:10: error: use of uninitialized value 'me.name' print(me) \# Error: use of uninitialized value 'me' ^ /var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-8g9tkr79/ch04-002.mojo:10:9: note: 'me' declared here var me = Person("Connor", 25) ^ /var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-8g9tkr79/ch04-002.mojo:11:29: warning: assignment to 'name\_owner' was never used; assign to '\_' instead? var name\_owner = me.name^ \# Move value from me.name. ^ /Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo: error: failed to run the pass manager After the move, the instance is still #emph[logically initialized], but it's only partially #emph[fieldwise initialized]. Mojo doesn't allow using instances with uninitialized fields. To make the instance usable again, you must reinitialize the moved-from field: var me = Person("Connor", 25) var name\_owner = me.name^ me.name = "John" \# Reinitialize field print(me) \# OK use\_value(name\_owner) \# Now, use name\_owner Any moved field requires a new value before you can use the instance again. Even if #emph[all] fields are moved, the instance itself remains logically initialized. === Mojo's default destructor \_\_del\_\_() is Mojo's default #emph[destructor] (or #emph[deinitializer]). While it runs, the instance remains logically initialized, and it becomes logically deinitialized when the method completes: \@fieldwise\_init struct Contact: var name: String var email: String def \_\_del\_\_(deinit self): \# \`self\` is initialized print("destroying contact") \# \`self is deinitialized def main(): var me = Contact("Alice", "alice\@example.com") \# Last use of \`me\` (and end of scope) \# Prints "destroying contact" along with a warning \# since the instance was never used after assignment. destroying contact === Destructors and moving fields Destructors are the only place where you can safely move values out of an instance's fields without having to reinitialize the field before its next use. This special rule isn't about where the code lives. It's about guarantees: def \_\_del\_\_(deinit self): var name = self.name^ \# OK: can take ownership of fields The compiler knows that destructors like \_\_del\_\_() are the final use of an instance. Because of this, it allows you to move values out of fields without reinitializing them. This ensures that fields are only moved out of struct instances when the compiler can guarantee the instance is at the end of its lifetime. Like instances, struct fields use ASAP destruction within deinit methods. For example: struct S: var a: String var b: String def \_\_del\_\_(deinit self): \# Mojo calls a.\_\_del\_\_() here. use(b) \# Mojo calls b.\_\_del\_\_() here. === The deinit convention A destructor's secret sauce is the deinit convention. Because of this, deinit self works in named destructors as well as \_\_del\_\_(): struct Parent: def consume(deinit self): \# \`consume\` is a named destructor \# Can move values out of fields here too When you use the deinit self convention in a method, the compiler recognizes the method as a destructor and lets you move a field without reinitializing it: struct Parent: def consume(deinit self): """valid destructor""" var name = self.name^ print("Name:", name) \# Prints name Any struct method that uses deinit self as its first argument is a destructor. As a destructor, you can use this consume method for explicit destruction types. === Explicit and implicit destructors The \_\_del\_\_() method is the core of implicit destruction types. It's Mojo's default destructor and works hand in hand with ImplicitlyDeletable, a trait that all structs conform to by default. The compiler calls \_\_del\_\_() automatically when it detects the final use of an implicitly deletable struct value. As the name suggests, explicit destruction requires you to call a destructor. An explicitly destroyed type must define at least one custom destructor, such as the consume() method shown in the previous section. The compiler make sure that the final use of an explicitly destroyed value will be a call to one of its custom destructors. ==== Using deinit with other arguments The deinit convention isn't limited to self. You can write methods and functions that destruct other instances: struct Pair: def destroy\_other(self, deinit other: Self): \# Can take from fields of \`other\` here Like deinit self, the deinit convention in this example tells the compiler that other is tagged for destruction. Using deinit means other is logically deinitialized at the end of the method. Because of this, it's safe to move values out of other, since the instance's lifetime is guaranteed to complete. === Normal methods and moving fields In ordinary methods, you can't move values out of a struct's fields without reinitializing them: \@fieldwise\_init struct MyStruct: var name: String def move\_field(mut self, var new\_name: String): var name = self.name^ \# Error \# error: 'self.name' is uninitialized at the implicit return \# from this function print("Name:", name) def main(): var instance = MyStruct("Ken") instance.move\_field("Scott") print("Name:", instance.name) \# Prints: "Name: Scott" /var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-8g9tkr79/ch04-004.mojo:5:9: error: 'self.name' is uninitialized at the implicit return from this function def move\_field(mut self, var new\_name: String): ^ /var/folders/vj/yj\_407r12vd0kndj7dxwxw740000gn/T/mojo-run-8g9tkr79/ch04-004.mojo:5:24: note: 'self' declared here def move\_field(mut self, var new\_name: String): ^ /Users/mbpro/FromOldMac/Claude/mojo-cas/.pixi/envs/default/bin/mojo: error: failed to run the pass manager To move safely, reinitialize the instance's fields before the end of scope. The following update fixes the bug by reinitializing the field: def move\_field(mut self, var new\_name: String): var name = self.name^ print("Name:", name) \# Prints name self.name = new\_name^ \# reinitialize the name field === Destroying instances Unless you opt into explicit destructors, Mojo uses implicit destruction. Both explicit and implicit destructors use ASAP ("As Soon As Possible") destruction: def example(): var x = SomeType() use(x) \# last use in scope \# x destroyed here, immediately after last use more\_code() \# x is already gone With ASAP, Mojo destroys instances immediately after their last use, not at the end of scope. This can happen even in the middle of an expression. This differs from many languages with compiler-managed lifecycles, where destruction happens at end of scope. Mojo's ASAP destruction enables better tail call optimization, but it also means destructors can run earlier than you might expect. Mojo also supports #emph[explicitly-destroyed types], which require you to make an explicit call to a destructor method. For more information, see Explicitly-destroyed types. Mojo tracks the lifetimes of values in your code. It knows when you move out of a value using the transfer operator (^), or when you call a deinit method that explicitly destroys it. If the compiler finds a live value that hasn't otherwise been cleaned up, it uses the implicit destructor (\_\_del\_\_(deinit self...)) to destroy it. If a type doesn't have an implicit destructor because it requires explicit destruction, the compiler emits an error, as indicated by the \@explicit\_destroy decorator. While the compiler checks that a destructor is called before an instance goes out of scope, you're responsible for choosing #emph[when] that happens. The key differences are: - Destruction doesn't automatically happen at the point of last use. - The explicit destructor isn't named \_\_del\_\_(). === Object initialization summary Creating, using, and destroying instances in Mojo follows a consistent set of rules based on initialization state. Create instances by calling \_\_init\_\_(). You must initialize every field in the initializer, especially if you plan to use the instance within the method scope. You can't call methods on a struct unless all its fields are initialized. You must #emph[logically] initialize an instance before using it, which means calling the initializer. You must also initialize fields before accessing them. Partially initialized instances can't be used in many contexts, including method calls and return values. For implicitly destroyed structs, the compiler inserts a destructor call after the instance's last use. For explicitly destroyed values, you must call a destructor before the instance goes out of scope. Destruction is compiler-checked, but you choose when it happens. The value's final use in scope must be that explicit destructor call. Field operations are also constrained. Fieldwise initialization must happen inside the \_\_init\_\_() constructor. You can move values out of a field only when the compiler can prove that the instance won't be used again, or that the field will be reinitialized before the end of a normal method. This split between logical and fieldwise initialization gives Mojo fine-grained control over instance lifetimes, while ensuring that initialization, use, and destruction remain safe and explicit.