A first look at Mojo. In this post I scratch the surface of Mojo’s syntax and compare how its borrow semantics compare to rust’s.
Mojo - An Introduction
Mojo is designed to be a superset of Python, bringing an array of features and capabilities that make it particularly well-suited for software development, especially in the field of artificial intelligence. In this article, we’ll explore some of the key differences that set Mojo apart from Python.
Syntax: fn
vs. def
& let
vs var
Before delving into Mojo’s unique features, let’s start with a basic syntax comparison. In Mojo, functions are defined using the fn
keyword, while Python uses def
. This distinction is just the tip of the iceberg when it comes to the differences between the two languages.
fn do_math():
let x: Int = 1
let y = 2
print(x + y)
>>> do_math()
3
In this example, do_math
is a simple function that calculates and prints the sum of two variables. The use of let
to declare variables, similar to var
in Python, shows that Mojo emphasizes immutability by default.
Function Arguments: Borrowed, Mutable, and Owned
One of Mojo’s standout features is that it is a statically typed language and its borrow semantics. In Mojo, function arguments are, by default, immutable references, which means they cannot be modified within the function. However, Mojo allows you to declare arguments as inout
to make them mutable, enabling changes that persist outside the function.
fn add(x: Int, y: Int) -> Int:
return x + y
>>> let x = 1
>>> let y = 1
>>> add(x, y)
2
The above function add
takes two immutable arguments and returns their sum. To make the arguments mutable, you would declare them as inout
.
fn add_inout(inout x: Int, inout y: Int) -> Int:
x += 1
y += 1
return x + y
>>> let x = 1
>>> let y = 1
>>> add(x, y)
4
>>>print(x)
2
>>>print(y)
2
Another option is to declare the argument as owned
, which provides the function full ownership of the value
, it’s mutable and guaranteed unique.
This way, the function can modify the value and not worry about affecting variables outside the function
If owned is not used then text
can’t be modified: error: expression must be mutable in assignment
fn add_surname(owned text: String) -> String:
text = text + " Lemos"
return text
>>> let name = "Bernardo"
>>> let name_w_surname = add_surname(name)
>>> print(name_w_surname)
Bernardo Lemos
>>> print(name)
Bernardo
Note how even though name
was mutated in add_surname
, changes did not materialize outside the function, since a copy was passed.
However, if you want to give the function ownership of the value and do not want to make a copy (which can be an expensive operation for some types), then you can add the ^ “transfer” operator when you pass a to the function. The transfer operator effectively destroys the local variable name—any attempt to call upon it later causes a compiler error.
>>> let name_w_surname = add_surname(name^) # transfer object
>>> print(name_w_surname)
Bernardo Lemos
>>> print(name) # raises an error, `name` no longer in scope
Structures: Static Abstractions
Mojo provides high-level abstractions for types using structures (similar to classes in Python). These structures support methods, fields, operator overloading, and metaprogramming, but they are entirely static, bound at compile-time, and do not allow dynamic dispatch.
struct MyPair:
var first: Int
var second: Int
fn __init__(inout self, first: Int, second: Int):
self.first = first
self.second = second
fn dump(self):
print(self.first, self.second)
Errors - Making it Crash
In this section we’ll explore some errors raised by mojo’s compiler, namely borrow checking.
Error 1: add_inout
Argument Mutability
error: invalid call to 'add_inout': argument #0 must be mutable in order to pass as a by-ref argument
let y = 3
add_inout(x, y)
Explanation: In this error, Mojo is enforcing its immutability rules. The function add_inout
expects mutable references as arguments. The attempt to pass an immutable reference, y
, causes a compilation error.
Error 2: Variable Shadowing
Mojo does not support shadowing. Trying to instantiate a variable of the same name raises an error.
let name_with_surname = add_surname(name)
...
let name_with_surname = "Other"
error: invalid redefinition of 'name_with_surname'
let name_with_surname = add_surname(name)"""
Explanation: Mojo does not support variable shadowing. In this case, attempting to redefine the variable name_with_surname
in the same scope results in an error. Variable names must be unique within their scope.
Error 3: Ownership and Transfer Operator
Transfering name
to the function will destroy name
it in the current scope
making it unusable from this point forward.
Running:
let name_with_surname_3 = add_surname(name^)
print(name)
Produces an error:
error: use of uninitialized value 'name'
print(name)
note: 'name' declared here
let name = "Berna"
Explanation: This error highlights Mojo’s ownership and transfer semantics. Using the ^
transfer operator when passing name
to the add_surname
function means that the function takes full ownership of the variable, making it unusable in the current scope. Attempting to use name
after transferring ownership results in an “uninitialized value” error.
Comparing Mojo to Rust Ownership Semantics
Mentioning Rust ownership semantics is misleading, since transferring an immutable object to a function in Rust, the function which then owns the variable cannot mutate it. To do so, the function must explicitly accept a mutable argument (mut text: String
), which will raise cannot borrow
text as mutable, as it is not declared as mutable
. On the contrary, Mojo allows the function to change the transferred value, even if it’s immutable, as seen in the example. But in Mojo’s docs, this is explicit: “owned
, which provides the function full ownership of the value (it’s mutable and guaranteed unique)
While Rust enforces stricter ownership rules, Mojo allows functions to change transferred values, even if they are originally immutable. Mojo’s owned
concept provides full ownership of a value, making it mutable and unique within the function.
Conclusion
Mojo is a powerful and promosing new language that pushes the boundaries of what’s possible in developing AI solutions. Its syntax and features offer a fresh perspective on software development, making it a valuable addition to the programming landscape.
Mojo introduces a unique set of rules and behaviors related to mutability, variable shadowing, and ownership. These features make Mojo an intriguing and distinctive language for developers exploring new paradigms in programming.
In future articles, we’ll explore Mojo in more depth and dive into advanced topics and work on ML use cases.
References