Value semantics
Mojo doesn't enforce value semantics or reference semantics. It supports them both and allows each type to define how it is created, copied, and moved (if at all). So, if you're building your own type, you can implement it to support value semantics, reference semantics, or a bit of both. That said, Mojo is designed with argument behaviors that default to value semantics, and it provides tight controls for reference semantics that avoid memory errors.
The controls over reference semantics are provided by the value ownership model, but before we get into the syntax and rules for that, it's important that you understand the principles of value semantics. Generally, it means that each variable has unique access to a value, and any code outside the scope of that variable cannot modify its value.
Intro to value semantics
In the most basic situation, sharing a value-semantic type means that you create a copy of the value. This is also known as "pass by value." For example, consider this code:
def main():
var x = 1
var y = x
y += 1
print("x:", x)
print("y:", y)
def main():
var x = 1
var y = x
y += 1
print("x:", x)
print("y:", y)
x: 1
y: 2
x: 1
y: 2
We assigned the value of x
to y
, which creates the value for y
by making a
copy of x
. When we increment y
, the value of x
doesn't change. Each
variable has exclusive ownership of a value.
Whereas, if a type instead uses reference semantics, then y
would point to
the same value as x
, and incrementing either one would affect the value for
both. Neither x
nor y
would "own" the value, and any variable would be
allowed to reference it and mutate it.
Numeric values in Mojo are value semantic because they're trivial types, which are cheap to copy.
Value semantics in Mojo functions
Value semantics also apply to function arguments in Mojo by default. However,
the way in which they apply differs depending on whether you define the function
using def
or fn
. You can also override the default behavior by providing an
explicit argument
convention, which is
discussed in the Ownership page.
Value semantics in def
functions
Here's an example with a function defined using def
:
def add_one(y: Int):
# def creates an implicit copy of the value because it's mutated
y += 1
print("y:", y)
def main():
var x = 1
add_one(x)
print("x:", x)
def add_one(y: Int):
# def creates an implicit copy of the value because it's mutated
y += 1
print("y:", y)
def main():
var x = 1
add_one(x)
print("x:", x)
y: 2
x: 1
y: 2
x: 1
Again, the y
value is a copy and the function cannot modify the original x
value.
If you're familiar with Python, this is probably familiar so far, because the code above behaves the same in Python. However, Python is not value semantic.
It gets complicated, but let's consider a situation in which you call a Python function and pass an object with a pointer to a heap-allocated value. Python actually gives that function a reference to your object, which allows the function to mutate the heap-allocated value. This can cause nasty bugs if you're not careful, because the function might incorrectly assume it has unique ownership of that object.
In Mojo, the default behavior for all function arguments is to use value semantics. If the function wants to modify the value of an incoming argument, then it must explicitly declare so, which avoids accidental mutations of the original value.
All Mojo types passed to a def
function can be treated as mutable,
which maintains the expected mutability behavior from Python. But by default, it
is mutating a uniquely-owned value, not the original value.
For example, when you pass an instance of a SIMD
vector to a def
function it creates a unique copy of all values. Thus, if we modify the
argument in the function, the original value is unchanged:
def update_simd(t: SIMD[DType.int32, 4]):
t[0] = 9
print("t:", t)
def main():
var v = SIMD[DType.int32, 4](1, 2, 3, 4)
update_simd(v)
print("v:", v)
def update_simd(t: SIMD[DType.int32, 4]):
t[0] = 9
print("t:", t)
def main():
var v = SIMD[DType.int32, 4](1, 2, 3, 4)
update_simd(v)
print("v:", v)
t: [9, 2, 3, 4]
v: [1, 2, 3, 4]
t: [9, 2, 3, 4]
v: [1, 2, 3, 4]
If this were Python code, the function would modify the original object, because Python shares a reference to the original object.
However, not all types are inexpensive to copy. Copying a String
or List
requires allocating heap memory, so we want to avoid copying one by accident.
When designing a type like this, ideally you want to prevent implicit copies,
and only make a copy when it's explicitly requested.
Value semantics in fn
functions
The arguments above are mutable because a function defined with def
has
special treatment for the default read
argument
convention.
In contrast, fn
functions always receive read
arguments as immutable
references. This is a memory optimization to avoid making
unnecessary copies.
For example, let's create another function with the fn
declaration. In this
case, the y
argument is immutable by default, so if the function wants to
modify the value in the local scope, it needs to make a local copy:
fn add_two(y: Int):
# y += 2 # This would cause a compiler error because `y` is immutable
# We can instead make an explicit copy:
var z = y
z += 2
print("z:", z)
def main():
var x = 1
add_two(x)
print("x:", x)
fn add_two(y: Int):
# y += 2 # This would cause a compiler error because `y` is immutable
# We can instead make an explicit copy:
var z = y
z += 2
print("z:", z)
def main():
var x = 1
add_two(x)
print("x:", x)
z: 3
x: 1
z: 3
x: 1
This is all consistent with value semantics because each variable maintains unique ownership of its value.
The way the fn
function receives the y
value is a "look but don't touch"
approach to value semantics. This is also a more memory-efficient approach when
dealing with memory-intensive arguments, because Mojo doesn't make any copies
unless we explicitly make the copies ourselves.
Thus, the default behavior for def
and fn
arguments is fully value
semantic: arguments are either copies or immutable references, and any living
variable from the callee is not affected by the function.
But we must also allow reference semantics (mutable references) because it's how we build performant and memory-efficient programs (making copies of everything gets really expensive). The challenge is to introduce reference semantics in a way that does not disturb the predictability and safety of value semantics.
The way we do that in Mojo is, instead of enforcing that every variable have "exclusive access" to a value, we ensure that every value has an "exclusive owner," and destroy each value when the lifetime of its owner ends.
On the next page about value ownership, you'll learn how to modify the default argument conventions, and safely use reference semantics so every value has only one owner at a time.
Was this page helpful?
Thank you! We'll create more content like this.
Thank you for helping us improve!