Lifetimes and references
In Mojo, lifetime has two meanings:
-
In general terms, a value's lifetime refers to the span of time when the value is valid.
-
It also refers to a specific type of parameter value used to help track the lifetimes of values and references to values. For clarity, we'll use
lifetime
in code font to refer to the type.
The Mojo compiler includes a lifetime checker, a compiler pass that analyzes dataflow through your program. It identifies when variables are valid and inserts destructor calls when a value's lifetime ends.
The Mojo compiler uses lifetime
values to track the validity of references.
Specifically, a lifetime
value answers two questions:
- What logical storage location "owns" this value?
- Can the value be mutated using this reference?
For example, consider the following code:
fn print_str(s: String):
print(s)
name = String("Joan")
print_str(name)
fn print_str(s: String):
print(s)
name = String("Joan")
print_str(name)
The line name = String("Joan")
declares a variable with an identifier (name
)
and logical storage space for a String
value. When you pass name
into the
print_str()
function, the function gets an immutable reference to the value.
So both name
and s
refer to the same logical storage space, and have
associated lifetime
values that lets the Mojo compiler reason about them.
Most of the time, lifetime
values are handled automatically by the compiler.
However, in some cases you'll need to interact with lifetime
values directly:
-
When working with references—specifically
ref
arguments andref
return values. -
When working with types like
Reference
orSpan
which are parameterized on thelifetime
of the data they refer to.
This section covers ref
arguments and
ref
return values, which let functions
take arguments and provide return values as references with parametric
lifetimes.
Working with lifetimes
Mojo's lifetime
values are unlike most other values in the language, because
they're primitive values, not Mojo structs. Specifying a parameter that takes a
lifetime
value, you can't just say, l: Lifetime
, because there's no
Lifetime
type. Likewise, because these values are mostly created by the
compiler, you can't just create your own lifetime
value—you usually need to
derive a lifetime
from an existing value.
Lifetime types
Mojo supplies a struct and a set of aliases that you can use to specify
lifetime
types. As the names suggest, the ImmutableLifetime
and
MutableLifetime
aliases represent immutable and mutable lifetimes,
respectively:
struct ImmutableRef[lifetime: ImmutableLifetime]:
pass
struct ImmutableRef[lifetime: ImmutableLifetime]:
pass
Or you can use the AnyLifetime
struct to specify a lifetime with parametric mutability:
struct ParametricRef[
is_mutable: Bool,
//,
lifetime: AnyLifetime[is_mutable].type
]:
pass
struct ParametricRef[
is_mutable: Bool,
//,
lifetime: AnyLifetime[is_mutable].type
]:
pass
Note that AnyLifetime
isn't a lifetime value, it's a helper for specifying a
lifetime
type. Lifetime types carry the mutability of a reference as a
boolean parameter value, indicating whether the lifetime is mutable, immutable,
or even with mutability depending on a parameter specified by the enclosing API.
The is_mutable
parameter here is an infer-only
parameter. It's never
specified directly by the user, but always inferred from context. The
lifetime
value is often inferred, as well. For example, the following code
creates a Reference
to an existing
value, but doesn't need to specify a lifetime—the lifetime
is inferred from
the variable passed in to the reference.
from memory import Reference
def use_reference():
a = 10
r = Reference(a)
from memory import Reference
def use_reference():
a = 10
r = Reference(a)
Lifetime values
Most lifetime
values are created by the compiler. As a developer, there are a
few ways to specify lifetime
values:
- Static lifetimes. The
ImmutableStaticLifetime
andMutableStaticLifetime
aliases are lifetimes that last for the duration of the program. - The
__lifetime_of()
magic function, which returns the lifetime associated with the value (or values) passed in. - Inferred lifetime. You can use inferred parameters to capture the lifetime of a value passed in to a function.
Static lifetimes
You can use the static lifetimes ImmutableStaticLifetime
and
MutableStaticLifetime
when you have a value that should never be destroyed;
or when there's no way to construct a meaningful lifetime
for a value.
For an example of the first case, the StringLiteral
method
as_string_slice()
returns a StringSlice
pointing
to the original string literal. String literals are static—they're allocated at
compile time and never destroyed—so the slice is created with an immutable,
static lifetime.
Converting an
UnsafePointer
into a
Reference
is an example of the second case: the UnsafePointer
's data
doesn't carry a lifetime
—one reason that it's considered unsafe—but you need
to specify a lifetime
when creating a Reference
. In this case, there's no
way to construct a meaningful lifetime
value, so the new Reference
is
constructed with a MutableStaticLifetime
. Mojo won't destroy this value
automatically. As with any value stored using a pointer, it's up to the user to
explicitly destroy the
value.
Derived lifetimes
Use the __lifetime_of()
magic function to obtain a value's lifetime. This can
be useful, for example, when creating a container type. Consider the List
type. Subscripting into a list (list[4]
) returns a reference to the item at
the specified position. The signature of the __getitem__()
method that's
called to return the subscripted item looks like this:
fn __getitem__(ref [_]self, idx: Int) -> ref [__lifetime_of(self)] T:
fn __getitem__(ref [_]self, idx: Int) -> ref [__lifetime_of(self)] T:
The syntax may be unfamiliar—ref
arguments and ref
return values are
described in the following sections. For now it's enough to know that
the return value is a reference of type T
(where T
is the element type
stored in the list), and the reference has the same lifetime as the list itself.
This means that as long as you hold the reference, the underlying list won't be
destroyed.
Inferred lifetimes
The other common way to access a lifetime value is to infer it from the
the arguments passed to a function or method. For example, the Span
type
has an associated lifetime
:
struct Span[
is_mutable: Bool, //,
T: CollectionElement,
lifetime: AnyLifetime[is_mutable].type,
](CollectionElementNew):
"""A non owning view of contiguous data.
struct Span[
is_mutable: Bool, //,
T: CollectionElement,
lifetime: AnyLifetime[is_mutable].type,
](CollectionElementNew):
"""A non owning view of contiguous data.
One of its constructors creates a Span
from an existing List
, and infers
its lifetime
value from the list:
fn __init__(inout self, ref [lifetime]list: List[T, *_]):
"""Construct a Span from a List.
Args:
list: The list to which the span refers.
"""
self._data = list.data
self._len = len(list)
fn __init__(inout self, ref [lifetime]list: List[T, *_]):
"""Construct a Span from a List.
Args:
list: The list to which the span refers.
"""
self._data = list.data
self._len = len(list)
Working with references
You can use the ref
keyword with arguments and return values to specify a
reference with parametric mutability. That is, they can be either mutable or
immutable.
These references shouldn't be confused with the Reference
type, which is
basically a safe pointer type. A Reference
needs to be dereferenced, like a
pointer, to access the underlying value. A ref
argument, on the other hand,
looks like a borrowed
or inout
argument inside the function. A ref
return
value looks like any other return value to the calling function, but it is a
reference to an existing value, not a copy.
ref
arguments
The ref
argument convention lets you specify an argument of parametric
mutability: that is, you don't need to know in advance whether the passed
argument will be mutable or immutable. There are several reasons you might want
to use a ref
argument:
-
You want to accept an argument with parametric mutability.
-
You want to tie the lifetime of one argument to the lifetime of another argument.
-
When you want an argument that is guaranteed to be passed in memory: this can be important and useful for generic arguments that need an identity, irrespective of whether the concrete type is register passable.
The syntax for a ref
argument is:
ref [lifetime] argName: argType
The lifetime
parameter passed inside the square brackets can be replaced with
an underscore character (_
) to indicate that the parameter is unbound. Think
of it as a wildcard that will accept any lifetime:
def add_ref(ref [_] a: Int, b: Int) -> Int:
return a+b
def add_ref(ref [_] a: Int, b: Int) -> Int:
return a+b
You can also name the lifetime explicitly. This is useful if you want to specify
an ImmutableLifetime
or MutableLifetime
, or if you want to bind to
the is_mutable
parameter.
def take_str_ref[
is_mutable: Bool, //,
life: AnyLifetime[is_mutable].type
](ref [life] s: String):
@parameter
if is_mutable:
print("Mutable: " + s)
else:
print("Immutable: " + s)
def pass_refs(s1: String, owned s2: String):
take_str_ref(s1)
take_str_ref(s2)
pass_refs("Hello", "Goodbye")
def take_str_ref[
is_mutable: Bool, //,
life: AnyLifetime[is_mutable].type
](ref [life] s: String):
@parameter
if is_mutable:
print("Mutable: " + s)
else:
print("Immutable: " + s)
def pass_refs(s1: String, owned s2: String):
take_str_ref(s1)
take_str_ref(s2)
pass_refs("Hello", "Goodbye")
ref
return values
Like ref
arguments, ref
return values allow a function to return a mutable
or immutable reference to a value. Like a borrowed
or inout
argument, these
references don't need to be dereferenced.
ref
return values can be an efficient way to handle updating items in a
collection. The standard way to do this is by implementing the __getitem__()
and __setitem__()
dunder methods. These are invoked to read from and write to
a subscripted item in a collection:
value = list[a]
list[b] += 10
value = list[a]
list[b] += 10
With a ref
argument, __getitem__()
can return a mutable reference that can
be modified directly. This has pros and cons compared to using a __setitem__()
method:
-
The mutable reference is more efficient—a single update isn't broken up across two methods. However, the referenced value must be in memory.
-
A
__getitem__()
/__setitem__()
pair allows for arbitrary to be run when values are retrieved and set. For example,__setitem__()
can validate or constrain input values.
For example, in the following example, NameList
has a get()
method
that returns a reference:
struct NameList:
var names: List[String]
def __init__(inout self, *names: String):
self.names = List[String]()
for name in names:
self.names.append(name[])
def __getitem__(ref [_] self: Self, index: Int) ->
ref [__lifetime_of(self)] String:
if (index >=0 and index < len(self.names)):
return self.names[index]
else:
raise Error("index out of bounds")
def use_name_list():
list = NameList("Thor", "Athena", "Dana", "Vrinda")
print(list[2])
list[2] += "?"
print(list[2])
use_name_list()
struct NameList:
var names: List[String]
def __init__(inout self, *names: String):
self.names = List[String]()
for name in names:
self.names.append(name[])
def __getitem__(ref [_] self: Self, index: Int) ->
ref [__lifetime_of(self)] String:
if (index >=0 and index < len(self.names)):
return self.names[index]
else:
raise Error("index out of bounds")
def use_name_list():
list = NameList("Thor", "Athena", "Dana", "Vrinda")
print(list[2])
list[2] += "?"
print(list[2])
use_name_list()
Note that this update succeeds, even though NameList
doesn't define a
__setitem__()
method:
list[2] += "?"
list[2] += "?"
Also note that the code uses the return value directly each time, rather than assigning the return value to a variable, like this:
name = list[2]
name = list[2]
Since a variable needs to own its value, name
would end up with an owned
copy of the value that list[2]
returns. Mojo doesn't currently have
syntax to express that you want to keep the original reference in name
. This
will be added in a future release.
In cases where you need to be able to assign the return value to a variable—for
example, an iterator which will be used in a for..in
loop—you might consider
returning a Reference
instead of a ref
return value. For example, see the
iterator for the List
type.
You can assign a Reference
to a variable, but you need to use the dereference
operator ([]
) to access the underlying value.
nums = List(1, 2, 3)
for item in nums: # List iterator returns a Reference
print(item[])
nums = List(1, 2, 3)
for item in nums: # List iterator returns a Reference
print(item[])
Parametric mutability of return values
Another advantage of ref
return arguments is the ability to support parametric
mutability. For example, recall the signature of the __getitem__()
method
above:
def __getitem__(ref [_] self: Self, index: Int) ->
ref [__lifetime_of(self)] String:
def __getitem__(ref [_] self: Self, index: Int) ->
ref [__lifetime_of(self)] String:
Since the lifetime
of the return value is tied to the lifetime of self
, the
returned reference will be mutable if the method was called using a
mutable reference. The method still works if you have an immutable reference
to the NameList
, but it returns an immutable reference:
fn pass_immutable_list(list: NameList) raises:
print(list[2])
# list[2] += "?" # Error, this list is immutable
def use_name_list_again():
list = NameList("Sophie", "Jack", "Diana")
pass_immutable_list(list)
use_name_list_again()
fn pass_immutable_list(list: NameList) raises:
print(list[2])
# list[2] += "?" # Error, this list is immutable
def use_name_list_again():
list = NameList("Sophie", "Jack", "Diana")
pass_immutable_list(list)
use_name_list_again()
Without parametric mutability, you'd need to write two versions of
__getitem__()
, one that accepts an immutable self
and another that accepts
a mutable self
.
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!