Self-referential structs
Some data structures don’t fit well with value semantics. Lists, trees, and graphs all need nodes that point to each other. You can’t build these by nesting one value inside another, because the type would keep growing forever.
In Mojo, you build these shapes with pointers, heap allocation, and manual cleanup. The idea may feel new at first, but the pattern stays simple once you see it in small steps.
Avoid direct self-reference
Mojo won't let you build a type that stores another instance of itself,
even when nested within an Optional:
struct Node:
var value: String
var next: Optional[Node] # ERROR: Recursive reference
# ...Each struct has a fixed layout. If Node held another Node directly, the
compiler wouldn’t know how much space to reserve. Optional fields don’t help,
because the outer value still needs room for the inner one.
Pointers solve this problem. Pointers have a fixed size, and they let values point at each other without blowing up the type.
Adding self-referential pointers
The following code shows how to set up a node that can point to its own type. This sample gives you a node type with a value slot and a single link to the next node:
struct Node[ElementType: ImplicitlyCopyable & Writable](Movable):
comptime NodePointer = UnsafePointer[Self, MutOrigin.external]
var value: Optional[Self.ElementType]
var next: Self.NodePointer
fn __init__(out self, value: Optional[Self.ElementType] = None):
self.value = value
self.next = Self.NodePointer()The code defines a type-specific NodePointer type alias built on
UnsafePointer.
Add the comptime within the struct definition, as shown here.
Including MutOrigin.external
lets the pointer represent dynamically-allocated memory that
won't be tracked by the lifetime checker. You'll need to both allocate
and deallocate memory as needed.
The optional value lets you create "empty" nodes, enabling you to move
new Node memory allocations into place.
Building nodes
Here’s the key pattern you’ll use in many reference structures:
- Allocate space.
- Construct a value-holding node.
- Move it into the allocated memory.
- Return the pointer.
And here's an example of that pattern:
@staticmethod
fn makeNode(value: Self.ElementType) -> Self.NodePointer:
var node_ptr = alloc[Self](1)
if not node_ptr:
abort("Out of memory")
node_ptr.init_pointee_move(Self(value))
return node_ptrThis "allocate space, initialize, and move" approach creates safe pointer-based structures in Mojo.
Linking nodes
To link nodes, create a new node and set your next pointer
to it. This sample shows how to append to a new node created
with a value you supply:
fn append(mut self, value: Self.ElementType):
self.next = Self.makeNode(value)Be careful. This simple version overwrites any existing next link.
If your list already has nodes here, make sure you handle them or free them first!
You’ll see ways to do that in the "Cleaning up" and "Destructors" sections below.
Walking the list
To walk the list, follow the chain until you hit the end. Recursive code makes this simple to read. This sample prints the value of each pointer:
@staticmethod
fn print_list(node_ptr: Self.NodePointer):
var current_value: Optional[Self.ElementType] = node_ptr[].value
if current_value:
print(current_value.value(), end=" ")
if node_ptr[].next:
Self.print_list(node_ptr[].next) # recurse
else:
print() # newlineThe pattern goes: check the value, print it if it exists, and walk to the next link.
Cleaning up
You allocated each node yourself, so you’re the one who frees them. This cleanup walks down the chain and frees each node as the recursion unwinds:
fn free_chain(self):
if self.next:
self.next[].free_chain()
self.next.destroy_pointee()
self.next.free()The “head” node stays allocated, unless you free it:
list_head[].free_chain()
list_head.destroy_pointee()
list_head.free()Destructors
When you build real Mojo data structures, you usually want a safe API that keeps users away from raw pointers. If you were putting together a complete linked-list type rather than a small demo of linkable nodes, the parent list would handle node allocation and freeing for you. It owns the nodes, so it also handles the cleanup in its destructor.
Here’s a small example from the Mojo Standard Library that shows how to
deinitialize self:
struct LinkedList[T]:
var _head: Self._NodePointer
fn __del__(deinit self):
"""Clean up the list by freeing all nodes.
Notes:
Time complexity: O(n) in len(self).
See Also:
"Choose the form of the Destructor!"
-- Gozer, "Ghostbusters" (1984).
"""
var curr = self._head
while curr:
var next = curr[].next
curr.destroy_pointee()
curr.free()
curr = nextWhat next?
- Learn more about pointers and memory safety in Mojo’s UnsafePointer and lifetime and origin rules guides.
- Learn more about how to manage cleanup in the Mojo destructor documentation.
- Learn more about generic structures with traits and how they let you build reusable node and list types.
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!