#set document(title: "4.3 Value creation", 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.3#h(0.6em)Value creation The life of a value in Mojo begins when a variable is initialized and continues up until the value is last used, at which point Mojo destroys it. This page describes how every value in Mojo is created, copied, and moved. (The next page describes how values are destroyed.) All data types in Mojo—including basic types in the standard library such as #link("https://mojolang.org/docs/std/builtin/bool/Bool/")[Bool], #link("https://mojolang.org/docs/std/builtin/int/Int/")[Int], and #link("https://mojolang.org/docs/std/collections/string/string/String/")[String]—are defined as structs. This means the creation and destruction of any piece of data follows the same lifecycle rules, and you can define your own data types that work exactly the same way. Mojo structs don't get any default lifecycle methods, such as a constructor, copy constructor, or move constructor. That means you can define a struct without a constructor, but then you can't instantiate it, and it would be useful only as a sort of namespace for static methods. For example: struct NoInstances: var state: Int \@staticmethod def print\_hello(): print("Hello world!") Without a constructor, this can't be instantiated, so it has no lifecycle. The state field is also useless because it can't be initialized (Mojo structs do not support default field values—you must initialize them in a constructor). So the only thing you can do is call the static method: NoInstances.print\_hello() Hello world! === Constructor To create an instance of a Mojo type, it needs the \_\_init\_\_() constructor method. The main responsibility of the constructor is to initialize all fields. For example: struct MyPet: var name: String var age: Int def \_\_init\_\_(out self, name: String, age: Int): self.name = name self.age = age Now we can create an instance: var mine = MyPet("Loki", 4) An instance of MyPet can also be read and destroyed, but it currently can't be copied or moved. We believe this is a good default starting point, because there are no built-in lifecycle events and no surprise behaviors. You—the type author—must explicitly decide whether and how the type can be copied or moved, by implementing the copy and move constructors. The pattern shown above—a constructor that takes an argument for each of the struct's fields and initializes the fields directly from the arguments—is called a #emph[field-wise constructor]. It's a common enough pattern that Mojo includes a #link("https://mojolang.org/docs/reference/decorators/fieldwise-init/")[\@fieldwise\_init] decorator to synthesize the field-wise constructor. So you can rewrite the previous example like this: \@fieldwise\_init struct MyPet: var name: String var age: Int #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Mojo does not require a destructor to destroy an instance. But in some cases, you may need to define a custom destructor to release resources (for example, if a struct dynamically allocates memory using #link("https://mojolang.org/docs/std/memory/unsafe_pointer/UnsafePointer/")[UnsafePointer]). We'll discuss that more in Death of a value. ] #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[The "constructor" name] In a Python class, object construction happens across both the \_\_new\_\_() and \_\_init\_\_() methods, so the \_\_init\_\_() method is technically just the initializer for attributes (but it's often still called the constructor). However, in a Mojo struct, there is no \_\_new\_\_() method, so we prefer to always call \_\_init\_\_() the constructor. ] ==== Overloading the constructor Like any other function/method, you can overload the \_\_init\_\_() constructor to initialize the object with different arguments. For example, you might want a default constructor that sets some default values and takes no arguments, and then additional constructors that accept more arguments. Just be aware that, in order to modify any fields, each constructor must declare the self argument with the out convention. If you want to call one constructor from another, you simply call upon that constructor as you would externally (you don't need to pass self). For example, here's how you can delegate work from an overloaded constructor: struct MyPet: var name: String var age: Int def \_\_init\_\_(out self): self.name = "" self.age = 0 def \_\_init\_\_(out self, name: String): self = MyPet() self.name = name ==== Field initialization Notice in the previous example that, by the end of each constructor, all fields must be initialized. That's the only requirement in the constructor. In fact, the \_\_init\_\_() constructor is smart enough to treat the self object as fully initialized even before the constructor is finished, as long as all fields are initialized. For example, this constructor can pass around self as soon as all fields are initialized: def use(arg: MyPet): pass struct MyPet: var name: String var age: Int def \_\_init\_\_(out self, name: String, age: Int, cond: Bool): self.name = name if cond: self.age = age use(self) \# Safe to use immediately! self.age = age use(self) \# Safe to use immediately! ==== Constructors and implicit conversion Mojo supports implicit conversion from one type to another. Implicit conversion can happen when one of the following occurs: - You assign a value of one type to a variable with a different type. - You pass a value of one type to a function that requires a different type. - You return a value of one type from a function that specifies a different return type. In all cases, implicit conversion is supported when the target type defines a constructor that meets the following criteria: - Is declared with the \@implicit decorator. - Has a single required, non-keyword argument of the source type. For example: var a = Source() var b: Target = a Mojo implicitly converts the Source value in a to a Target value if Target defines a matching constructor like this: struct Target: \@implicit def \_\_init\_\_(out self, s: Source): ... With implicit conversion, the assignment above is essentially identical to: var b = Target(a) In general, types should only support implicit conversions when the conversion is lossless and ideally inexpensive. For example, converting an integer to a floating-point number is usually lossless (except for very large positive and negative integers, where the conversion may be approximate), but converting a floating-point number to an integer is very likely to lose information. So Mojo supports implicit conversion from Int to Float64, but not the reverse. The constructor used for implicit conversion can take optional arguments, so the following constructor would also support implicit conversion from Source to Target: struct Target: \@implicit def \_\_init\_\_(out self, s: Source, reverse: Bool = False): ... Implicit conversion can fail if Mojo can't unambiguously match the conversion to a constructor. For example, if the target type has two overloaded constructors that take different types, and each of those types supports an implicit conversion from the source type, the compiler has two equally-valid paths to convert the values: struct A: \@implicit def \_\_init\_\_(out self, s: Source): ... struct B: \@implicit def \_\_init\_\_(out self, s: Source): ... struct OverloadedTarget: \@implicit def \_\_init\_\_(out self, a: A): ... \@implicit def \_\_init\_\_(out self, b: B): ... var t = OverloadedTarget(Source()) \# Error: ambiguous call to '\_\_init\_\_': each \# candidate requires 1 implicit conversion In this case, you can fix the issue by explicitly casting to one of the intermediate types. For example: var t = OverloadedTarget(A(Source())) \# OK Mojo applies at most one implicit conversion to a variable. For example: var t: OverloadedTarget = Source() \# Error: can't implicitly convert Source \# to Target Would fail because there's no direct conversion from Source to OverloadedTarget. For structs with a single field, you can generate an implicit constructor with the \@fieldwise\_init("implicit") decorator. \@fieldwise\_init("implicit") struct Counter: var count: Int def main(): var c: Counter = 5 \# implicitly converts from Int === Copy constructor In Mojo, a value can be copied either #emph[explicitly] or #emph[implicitly]: \# Explicit copy var s = "Test string" var s2 = s.copy() \# Implicit copy var i = 15 var i2 = i To make a struct explicitly copyable, you need to: - Add the Copyable trait. - (Optionally) define a custom copy constructor if needed, written def \_\_init\_\_(out self, \*, copy: Self). By adding the Copyable trait, Mojo can generate a default copy constructor for you unless you've written one yourself. This default method copies each field of the copy value into the new value. The Copyable trait also defines a default copy() method which provides an additional way to copy a value, versus calling the copy constructor directly. \@fieldwise\_init struct MyPet(Copyable): var name: String var age: Int Now this code works to make a copy: var mine = MyPet("Loki", 4) var yours = mine.copy() #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Technically, you #emph[could] make a struct with a copy constructor and #emph[not] add the Copyable trait, but this is not recommended. Mojo would be able to copy the value, but you couldn't use the struct with any generic containers or functions that require the Copyable trait. ] The generated copy constructor simply copies each field from the existing value into the new value. For example, if you manually wrote the copy constructor for MyPet, it would look like this: def \_\_init\_\_(out self, \*, copy: Self): self.name = copy.name self.age = copy.age This default copy constructor works in most cases, but there are a few cases where you need to define a custom copy constructor: - One or more of the struct's fields is not Copyable. - The struct includes a non-owning type (like #link("https://mojolang.org/docs/std/memory/unsafe_pointer/UnsafePointer/")[UnsafePointer]), and you want to make a deep copy of the data. - The struct holds other resources (like file descriptors or network sockets) that need to be managed. ==== Custom copy constructor What makes Mojo's copy behavior different, compared to other languages, is that copy constructor is designed to perform a deep copy of all fields in the type (as per value semantics). That is, it copies heap-allocated values, rather than just copying the pointer. However, the Mojo compiler doesn't enforce this, so it's the type author's responsibility to implement copy constructor with value semantics. For example, here's a new HeapArray type with a custom copy constructor that performs a deep copy: struct HeapArray(Copyable): var data: UnsafePointer\[Int, MutUntrackedOrigin\] var size: Int var cap: Int def \_\_init\_\_(out self, size: Int, val: Int): self.size = size self.cap = size \* 2 self.data = alloc\[Int\](self.cap) for i in range(self.size): (self.data + i).init\_pointee\_copy(val) def \_\_init\_\_(out self, \*, copy: Self): \# Deep-copy the existing value self.size = copy.size self.cap = copy.cap self.data = alloc\[Int\](self.cap) for i in range(self.size): (self.data + i).init\_pointee\_copy(copy.data\[i\]) \# The lifetime of \`copy\` continues unchanged def \_\_del\_\_(deinit self): \# We must free the heap-allocated data, but \# Mojo knows how to destroy the other fields for i in range(self.size): (self.data + i).destroy\_pointee() self.data.free() def append(mut self, val: Int): \# Update the array for demo purposes if self.size \< self.cap: (self.data + self.size).init\_pointee\_copy(val) self.size += 1 else: print("Out of bounds") def dump(self): \# Print the array contents for demo purposes print("\[", end="") for i in range(self.size): if i \> 0: print(", ", end="") print(self.data\[i\], end="") print("\]") Notice that the copy constructor doesn't copy the UnsafePointer value (doing so would make the copied value refer to the same data memory address as the original value, which is a shallow copy). Instead, we initialize a new UnsafePointer to allocate a new block of memory, and then copy over all the heap-allocated values (this is a deep copy). Thus, when we copy an instance of HeapArray, each copy has its own set of values on the heap, so changes to one array do not affect the other, as shown here: def copies(): var a = HeapArray(2, 1) var b = a.copy() \# Calls the copy method a.dump() \# Prints \[1, 1\] b.dump() \# Prints \[1, 1\] b.append(2) \# Changes the copied data b.dump() \# Prints \[1, 1, 2\] a.dump() \# Prints \[1, 1\] (the original did not change) Two other things to note from the copy constructor: - The copy argument is type Self (capital "S"). Self is an alias for the current type name (HeapArray, in this example). Using this alias is a best practice to avoid any mistakes when referring to the current struct name. - The copy argument is immutable because the default argument convention is an immutable reference. This is a good thing because this function shouldn't modify the contents of the value being copied. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ In HeapArray, we must use the \_\_del\_\_() destructor to free the heap-allocated data when the HeapArray lifetime ends, but Mojo automatically destroys all other fields when their respective lifetimes end. We'll discuss this destructor more in Death of a value. ] If your type doesn't use any pointers for heap-allocated data, then writing the constructor and copy constructor is all boilerplate code that you shouldn't have to write. For most structs that don't manage memory explicitly, you can just add the Copyable trait to your struct definition, and Mojo will synthesize the copy constructor. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ Mojo also calls upon the copy constructor when a value is passed to a function that takes the argument as var #emph[and] when the lifetime of the given value does #emph[not] end at that point. If the lifetime of the value does end there (usually indicated with the transfer sigil ^), then Mojo instead invokes the move constructor. ] ==== Implicitly-copyable types To make a type #emph[implicitly copyable], add the ImplicitlyCopyable trait: \@fieldwise\_init struct MyPair(ImplicitlyCopyable): var first: Int var second: Int def main(): pair = MyPair(3, 4) copy = pair print(pair.first, copy.second) 3 4 The ImplicitlyCopyable trait refines Copyable, so adding ImplicitlyCopyable to your type gives you Copyable's copy constructor and a copy() method. It also gives you automatic Movable conformance. The trait doesn't define any methods of its own: it serves as a signal to the compiler that it can insert calls to copy constructor as needed. For example, in the previous example, the compiler might generate code equivalent to: pair = MyPair(3, 4) copy = pair.copy() A type should be implicitly copyable only 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. In particular, any type that dynamically allocates memory or manages other resources probably shouldn't be implicitly copyable. === Move constructor Although copying values provides predictable behavior that matches Mojo's value semantics, copying some data types can be a significant hit on performance. Mojo uses the move constructor to transfer ownership of a value from one variable to another, #emph[without] copying its fields. A value that's copyable but doesn't have a move constructor can still be transferred by making a copy and then discarding the original. So in this case, the move constructor serves as an optimization. To make a type #emph[movable]: - Add the Movable trait #emph[or] conform to Copyable. - (Optionally) implement a custom move constructor. Here's a movable (also copyable) version of the MyPet struct: \@fieldwise\_init struct MyPet(Copyable): var name: String var age: Int Here's an example showing how to invoke the move constructor for MyPet: def moves(): var a = MyPet("Bobo", 2) print(a.name) \# Prints "bobo" var b = a^ \# the lifetime of \`a\` ends here print(b.name) \# prints "bobo" \# print(a.name) \# ERROR: use of uninitialized value 'a' If you include the Movable trait and don't define a move constructor, Mojo generates a default move constructor for you. This move constructor simply moves each of the fields to the new instance. The generated move constructor for MyPet would look like this if you wrote it yourself: def \_\_init\_\_(out self, \*, deinit move: Self): self.name = move.name^ self.age = move.age The move constructor takes its move argument using the deinit passing convention, which grants exclusive ownership of the value and marks it as destroyed at the end of the function. (For more information on the deinit convention, see Field lifetimes during destruct and move). The move constructor uses the transfer sigil (^) to indicate that ownership of the name value transfers from move to self. For trivial register-passable types like Int, omit the transfer sigil. Trivial types are always movable. Since they can't define custom move constructors or destructors, there's no special logic required to move. At the end of the move constructor method, Mojo immediately invalidates the original variable, preventing any access to it. Because the argument is declared as deinit, it does #emph[not] call the destructor, since that would destroy resources that have been transferred to the new instance. Invalidating the original variable is important to avoid memory errors on heap-allocated data, such as use-after-free and double-free errors. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ A move constructor is #emph[not required] to transfer ownership of a value. Mojo can copy the value and invalidate the original instance. You can learn more in the section about ownership transfer. If copying a type is expensive, moving it with a move constructor is much more efficient. For example, if a type has heap-allocated data, a copy constructor typically needs to allocate new storage to make a deep copy of the data. For types without heap-allocated fields, you get no real benefit from the move constructor. Making copies of simple data types on the stack, like integers, floats, and booleans, is very cheap. Yet, if you allow your type to be copied, then there's generally no reason to disallow moves. ] ==== Custom move constructor In practice, structs very rarely require a custom move constructor. A type might require a custom implementation if it has a pointer to itself or one of its fields, for example, since the struct's location in memory changes when it's moved. The move constructor performs a consuming move: it transfers ownership of a value from one variable to another when the original variable's lifetime ends (also called a "destructive move"). A critical feature of move constructors is that it takes the incoming value as a deinit argument, meaning this method gets unique ownership of the value. Moreover, because this is a dunder method that Mojo calls only when performing a move (during ownership transfer), the move argument is guaranteed to be a mutable reference to the original value, #emph[not a copy] (unlike other methods that may declare an argument as var, but might receive the value as a copy if the method is called without the ^ transfer sigil). That is, Mojo calls this move constructor #emph[only] when the original variable's lifetime actually ends at the point of transfer. === Move-only and immovable types To ensure your type can't be implicitly copied, you can make it "move-only" by making it Movable but not Copyable. A move-only type can be passed to other variables and passed into functions with any passing convention (like mut or var.) The only catch is that you must use the ^ transfer sigil to end the lifetime of a move-only type when assigning it to a new variable or when passing it as a var argument. The #link("https://mojolang.org/docs/std/memory/owned_pointer/OwnedPointer/")[OwnedPointer] is an example of a move-only type: because it is designed to provide clear single ownership of a stored value, the OwnedPointer can be moved, but not copied. In some (rare) cases, you may not want a type to be copyable #emph[or] movable. The #link("https://mojolang.org/docs/std/atomic/atomic/Atomic/")[Atomic] type is an example of a type that's neither copyable nor movable. #notebox("Note", rgb("#8a94a6"), rgb("#556666"), rgb("#f7f8fa"))[ #emph[Trivial types] Trivial types like Int, Bool, or Float64 are the most common types in systems programming. Mojo doesn't need special support for these. They have no ownership semantics, no destructors, and are always passed in CPU registers. ] ==== Trivial lifecycle methods The use of a decorator to identify trivial types will be phased out in favor of a more granular set of aliases, which are boolean flags set by the compiler: - The Copyable trait defines the alias \_\_copy\_ctor\_is\_trivial, which is an optimization hint that guarantees that the value can be copied by its bits from one location to another without causing any side effects. - The Movable trait defines the alias \_\_move\_ctor\_is\_trivial, which is an optimization hint that guarantees that the value can be moved by its bits from one location to another without causing any side effects. - The AnyType trait defines the alias \_\_del\_\_is\_trivial, which is a hint that the \_\_del\_\_() method is a no-op.