Mojo Tutorial
Mojo Tutorial
the concepts that you know in Python translate directly to Mojo. For instance, a "Hello World"
program in Mojo looks exactly as it does in Python:
[ ]:
print("Hello Mojo!")
And as we'll show later, you can also import existing Python packages and use them like you're
used to.
But Mojo provides a ton of powerful features on top of Python, so that's what we'll focus on in this
notebook.
To be clear, this guide is not your traditional introduction to a programming language. This
notebook assumes you're already familiar Python and some systems programming concepts so we
can focus on what's special about Mojo.
This runnable notebook is actually based on the Mojo programming manual, but we've simplified
a bunch of the explanation so you can focus on playing with the code. If you want to learn more
about a topic, refer to the complete manual.
let and var declarations
Exactly like Python you can assign values to a name and it implicitly creates a function-scope
variable within a function. This provides a very dynamic and easy way to write code, but it also
creates a challenge for two reasons:
if c != b:
let d = b
print(d)
your_function(2, 3)
let and var declarations also support type specifiers, patterns, and late initialization:
[ ]:
def your_function():
let x: Int = 42
let y: F64 = 17.0
let z: F32
if x != 0:
z = 1.0
else:
z = foo()
print(z)
your_function()
struct types
Modern systems programming have the ability to build high-level and safe abstractions on top of
low-level data layout controls, indirection-free field access, and other niche tricks. Mojo provides
that with the struct type.
struct types are similar in many ways to classes. However, where classes are extremely dynamic
with dynamic dispatch, monkey-patching (or dynamic method "swizzling"), and dynamically
bound instance properties, structs are static, bound at compile time, and are inlined into their
container instead of being implicitly indirect and reference counted.
struct MyPair:
var first: Int
var second: Int
Struct fields are bound statically: they aren't looked up with a dictionary indirection. As such, you
cannot del a method or reassign it at runtime. This enables the Mojo compiler to perform
guaranteed static dispatch, use guaranteed static access to fields, and inline a struct into the stack
frame or enclosing type that uses it without indirection or other overheads.
One of the primary ways to employ strong type checking is with Mojo's struct type.
A struct definition in Mojo defines a compile-time-bound name, and references to that name in a
type context are treated as a strong specification for the value being defined. For example,
consider the following code that uses the MyPair struct shown above:
[ ]:
Essentially, this allows you to define multiple functions with the same name but with different
arguments. This is a common feature seen in many languages such as C++, Java, and Swift.
struct Complex:
var re: F32
var im: F32
Mojo doesn't support overloading solely on result type, and doesn't use result type or contextual
type information for type inference, keeping things simple, fast, and predictable. Mojo will never
produce an "expression too complex" error, because its type-checker is simple and fast by
definition.
fn definitions
The extensions above are the cornerstone that provides low-level programming and provide
abstraction capabilities, but many systems programmers prefer more control and predictability
than what def in Mojo provides. To recap, def is defined by necessity to be very dynamic, flexible
and generally compatible with Python: arguments are mutable, local variables are implicitly
declared on first use, and scoping isn’t enforced. This is great for high level programming and
scripting, but is not always great for systems programming. To complement this, Mojo provides
an fn declaration which is like a “strict mode” for def.
fn and def are always interchangeable from an interface level: there is nothing a def can provide
that a fn cannot (or vice versa). The difference is that a fn is more limited and controlled on
the inside of its body (alternatively: pedantic and strict). Specifically, fns have a number of
limitations compared to defs:
1.
Argument values default to being immutable in the body of the function (like a let),
instead of mutable (like a var). This catches accidental mutations, and permits the use of
non-copyable types as arguments.
2.
3.
Argument values require a type specification (except for self in a method), catching
accidental omission of type specifications. Similarly, a missing return type specifier is
interpreted as returning None instead of an unknown return type. Note that both can be
explicitly declared to return object, which allows one to opt-in to the behavior of a def if
desired.
4.
5.
Implicit declaration of local variables is disabled, so all locals must be declared. This
catches name typos and dovetails with the scoping provided by let and var.
6.
7.
Both support raising exceptions, but this must be explicitly declared on a fn with
the raises function effect, placed after the function argument list.
8.
The __copyinit__ and __moveinit__ special methods
Mojo supports full "value semantics" as seen in languages like C++ and Swift, and it makes
defining simple aggregates of fields very easy with its @value decorator (described in more detail
in the Programming Manual).
For advanced use cases, Mojo allows you to define custom constructors (using Python's
existing __init__ special method), custom destructors (using the existing __del__ special method)
and custom copy and move constructors using the new __copyinit__ and __moveinit__ special
methods.
These low-level customization hooks can be useful when doing low level systems programming,
e.g. with manual memory management. For example, consider a heap array type that needs to
allocate memory for the data when constructed and destroy it when the value is destroyed:
[ ]:
fn __init__(inout self):
self.cap = 16
self.size = 0
self.data = Pointer[Int].alloc(self.cap)
fn __del__(owned self):
self.data.free()
fn dump(self):
print_no_newline("[")
for i in range(self.size):
if i > 0:
print_no_newline(", ")
print_no_newline(self.data.load(i))
print("]")
This array type is implemented using low level functions to show a simple example of how this
works. However, if you go ahead and try this out, you might be surprised:
[ ]:
var a = HeapArray(3, 1)
a.dump() # Should print [1, 1, 1]
# Uncomment to see an error:
# var b = a # ERROR: Vector doesn't implement __copyinit__
var b = HeapArray(4, 2)
b.dump() # Should print [2, 2, 2, 2]
a.dump() # Should print [1, 1, 1]
The compiler isn’t allowing us to make a copy of our array: HeapArray contains an instance
of Pointer (which is equivalent to a low-level C pointer), and Mojo can’t know “what the pointer
means” or “how to copy it” - this is one reason why application level programmers should use
higher level types like arrays and slices! More generally, some types (like atomic numbers) cannot
be copied or moved around at all, because their address provides an identity just like a class
instance does.
In this case, we do want our array to be copyable around, and to enable this, we implement
the __copyinit__ special method, which conventionally looks like this:
[ ]:
struct HeapArray:
var data: Pointer[Int]
var size: Int
var cap: Int
fn __init__(inout self):
self.cap = 16
self.size = 0
self.data = Pointer[Int].alloc(self.cap)
fn __del__(owned self):
self.data.free()
fn dump(self):
print_no_newline("[")
for i in range(self.size):
if i > 0:
print_no_newline(", ")
print_no_newline(self.data.load(i))
print("]")
With this implementation, our code above works correctly and the b = a copy produces a logically
distinct instance of the array with its own lifetime and data. Mojo also supports
the __moveinit__ method which allows both Rust-style moves (which take a value when a lifetime
ends) and C++-style moves (where the contents of a value is removed but the destructor still runs),
and allows defining custom move logic. Please see the Value Lifecycle section in the
Programming Manual for more information.
[ ]:
var a = HeapArray(3, 1)
a.dump() # Should print [1, 1, 1]
# This is no longer an error:
var b = a
Python integration
It's easy to use Python modules you know and love in Mojo. You can import any Python module
into your Mojo program and create Python types from Mojo types.
Currently, you cannot import individual members (such as a single Python class or function)—you
must import the whole Python module and then access members through the module name.
There's no need to worry about memory management when using Python in Mojo. Everything just
works because Mojo was designed for Python from the beginning.
For example, given this Python function that prints Python types:
[ ]:
%%python
def type_printer(my_list, my_tuple, my_int, my_string, my_float):
print(type(my_list))
print(type(my_tuple))
print(type(my_int))
print(type(my_string))
print(type(my_float))
You can pass the Python function Mojo types, no problem:
[ ]:
Mojo doesn't have a standard Dictionary yet, so it is not yet possible to create a Python dictionary
from a Mojo dictionary. You can work with Python dictionaries in Mojo though!
Parameterization: compile time meta-
programming
Mojo supports a full compile-time metaprogramming functionality built into the compiler as a
separate stage of compilation - after parsing, semantic analysis, and IR generation, but before
lowering to target-specific code. It uses the same host language for runtime programs as it does for
metaprograms, and leverages MLIR to represent and evaluate these programs in a predictable
way.
Here is very simplified and cut down version of the SIMD API from the Mojo standard library.
We use HeapArray to store the SIMD data for this example and implement basic operations on our
type using loops - we do that simply to mimic the desired SIMD type behavior for the sake of
demonstration. The real Stdlib implementation is backed by real SIMD instructions which are
accessed through Mojo's ability to use MLIR directly (see more on that topic in the Advanced
Mojo Features section).
[ ]:
fn dump(self):
self.value.dump()
Parameters in Mojo are declared in square brackets using an extended version of the PEP695
syntax. They are named and have types like normal values in a Mojo program, but they are
evaluated at compile time instead of runtime by the target program. The runtime program may use
the value of parameters - because the parameters are resolved at compile time before they are
needed by the runtime program - but the compile time parameter expressions may not use runtime
values.
In the example above, there are two declared parameters: the MySIMD struct is parameterized by
a size parameter, and concat method is further parametrized with an rhs_size parameter.
Because MySIMD is a parameterized type, the type of a self argument carries the parameters - the
full type name is MySIMD[size]. While it is always valid to write this out (as shown in the return
type of _add__), this can be verbose: we recommend using the Self type (from PEP673) like
the __sub__ example does.
The actual SIMD type provided by Mojo Stdlib is also parametrized on a data type of the
elements.
let f = d.concat[2](e)
f.dump()
let y = f + a
y.dump()
Note that the concat method needs an additional parameter to indicate the size of the second SIMD
vector: that is handled by parameterizing the call to concat. Our toy SIMD type shows the use of a
concrete type (Int), but the major power of parameters comes from the ability to define parametric
algorithms and types, e.g. it is quite easy to define parametric algorithms, e.g. ones that are length-
and DType-agnostic:
[ ]:
let a = MySIMD[2](1, 2)
let x = concat[2,2](a, a)
x.dump()
Note how the result length is the sum of the input vector lengths, and you can express that with a
simple + operation. For a more complex example, take a look at the SIMD.shuffle method in the
standard library: it takes two input SIMD values, a vector shuffle mask as a list, and returns a
SIMD that matches the length of the shuffle mask.
let x = MySIMD[4](1, 2, 3, 4)
x.dump()
print("Elements sum:", reduce_add[4](x))
This makes use of the @parameter if feature, which is an if statement that runs at compile time. It
requires that its condition be a valid parameter expression, and ensures that only the live branch of
the if is compiled into the program.
fn __init__(inout self):
self.cap = 16
self.size = 0
self.data = Pointer[Type].alloc(self.cap)
fn __del__(owned self):
self.data.free()
struct dtype:
alias invalid = 0
alias bool = 1
alias si8 = 2
alias ui8 = 3
alias si16 = 4
alias ui16 = 5
alias f32 = 15
This allows clients to use DType.f32 as a parameter expression (which also works as a runtime
value of course) naturally.
Types are another common use for alias: because types are just compile time expressions, it is
very handy to be able to do things like this:
[ ]:
Even vector length can be difficult to manage, because the vector length of a typical machine
depends on the datatype, and some datatypes like bfloat16 don't have full support on all
implementations. Mojo helps by providing an autotune function in the standard library. For
example, if you want to write a vector-length-agnostic algorithm to a buffer of data, you might
write it like this:
[ ]:
let N = 32
let a = DTypePointer[DType.f32].alloc(N)
let b = DTypePointer[DType.f32].alloc(N)
let res = DTypePointer[DType.f32].alloc(N)
# Initialize arrays with some values
for i in range(N):
a.store(i, 2.0)
b.store(i, 40.0)
res.store(i, -1)
buffer_elementwise_add[DType.f32](a, b, res, N)
print(a.load(10), b.load(10), res.load(10))
When compiling instantiations of this code Mojo forks compilation of this algorithm and decides
which value to use by measuring what works best in practice for the target hardware. It evaluates
the different values of the vector_len expression and picks the fastest one according to a user-
defined performance evaluator. Because it measures and evaluates each option individually, it
might pick a different vector length for F32 than for SI8, for example. This simple feature is pretty
powerful - going beyond simple integer constants - because functions and types are also parameter
expressions.
In the example above we didn't define the performance evaluator function, and the compiler just
picked one of the available implementations. However, we dive deep into how to do that in other
notebooks: we recommend checking out Matrix Multiplication and Fast Memset in Mojo.
Mojo on the other hand provides full control over value copies, aliasing of references, and
mutations.
By-reference arguments
Let’s start with the simple case: passing mutable references to values vs passing immutable
references. As we already know, arguments that are passed to fn’s are immutable by default:
[ ]:
struct MyInt:
var value: Int
fn __init__(inout self, v: Int):
self.value = v
fn __copyinit__(inout self, other: MyInt):
self.value = other.value
The problem here is that __iadd__ needs to mutate the internal state of the integer. The solution in
Mojo is to declare that the argument is passed “inout“ by using the inout marker on the argument
name (self in this case):
[ ]:
struct MyInt:
var value: Int
fn __init__(inout self, v: Int):
self.value = v
var x = 42
x += 1
print(x) # prints 43 of course
var a = Array[Int](16, 0)
a[4] = 7
a[4] += 1
print(a[4]) # Prints 8
let y = x
# Uncomment to see the error:
# y += 1 # ERROR: Cannot mutate 'let' value
Mojo implements the in-place mutation of the Array element by emitting a call
to __getitem__ into a temporary buffer, followed by a store with __setitem__ after the call.
Mutation of the let value fails because it isn’t possible to form a mutable reference to an
immutable value. Similarly, the compiler rejects attempts to use a subscript with a by-ref argument
if it implements __getitem__ but not __setitem__.
There is nothing special about self in Mojo, and you can have multiple different by-ref arguments.
For example, you can define and use a swap function like this:
[4]:
var x = 42
var y = 12
print(x, y) # Prints 42, 12
swap(x, y)
print(x, y) # Prints 12, 42
“Borrowed” argument convention
Now that we know how by-reference argument passing works, you may wonder how by-value
argument passing works and how that interacts with the __copyinit__ method which implements
copy constructors. In Mojo, the default convention for passing arguments to functions is to pass
with the “borrowed” argument convention. You can spell this out explicitly if you’d like:
[ ]:
let a = SomethingBig(10)
let b = SomethingBig(20)
use_something_big(a, b)
This default applies to all arguments uniformly, including the self argument of methods. The
borrowed convention passes an immutable reference to the value from the caller’s context, instead
of copying the value. This is much more efficient when passing large values, or when passing
expensive values like a reference counted pointer (which is the default for Python/Mojo classes),
because the copy constructor and destructor don’t have to be invoked when passing the argument.
Here is a more elaborate example building on the code above:
[ ]:
fn try_something_big():
# Big thing sits on the stack: after we construct it it cannot be
# moved or copied.
let big = SomethingBig(30)
# We still want to do useful things with it though!
big.print_id()
# Do other things with it.
use_something_big(big, big)
try_something_big()
Because the default argument convention is borrowed, we get very simple and logical code which
does the right thing by default: for example, we don’t want to copy or move all
of SomethingBig just to invoke the print_id method, or when calling use_something_big.
The borrowed convention is similar and has precedent to other languages. For example, the
borrowed argument convention is similar in some ways to passing an argument by const& in C++.
This avoids a copy of the value, and disables mutability in the callee. The borrowed convention
differs from const& in C++ in two important ways though:
1.
The Mojo compiler implements a borrow checker (similar to Rust) that prevents code
from dynamically forming mutable references to a value when there are immutable
references outstanding, and prevents having multiple mutable references to the same
value. You are allowed to have multiple borrows (as the call to use_something_big does
above) but cannot pass something by mutable reference and borrow at the same time.
(TODO: Not currently enabled).
2.
3.
Small values like Int, Float, and SIMD are passed directly in machine registers instead of
through an extra indirection (this is because they are declared with
the @register_passable decorator, see below). This is a significant performance
enhancement when compared to languages like C++ and Rust, and moves this
optimization from every call site to being declarative on a type.
4.
Rust is another important language and the Mojo and Rust borrow checkers enforce the same
exclusivity invariants. The major difference between Rust and Mojo is that no sigil is required on
the caller side to pass by borrow, Mojo is more efficient when passing small values, and Rust
defaults to moving values by default instead of passing them around by borrow. These policy and
syntax decisions allows Mojo to provide an arguably easier to use programming model.
For example, consider working with a move-only type like a unique pointer:
[ ]:
# This is not really a unique pointer, we just model its behavior here:
struct UniquePointer:
var ptr: Int
fn __del__(owned self):
self.ptr = 0
If we try copying it, we would correctly get an error:
[ ]:
let p = UniquePointer(100)
# Uncomment to see the error:
# let q = p # ERROR: value of type 'UniquePointer' cannot be copied into
its destination
While the borrow convention makes it easy to work with the unique pointer without ceremony, at
some point you may want to transfer ownership to some other function. This is what the ^ operator
does.
For movable types, the ^ operator ends the lifetime of a value binding and transfers the value to
something else (in this case, the take_ptr function). To support this, you can define functions as
taking owned arguments, e.g. you define take_ptr like so:
[ ]:
fn use_ptr(borrowed p: UniquePointer):
print("use_ptr")
print(p.ptr)
fn take_ptr(owned p: UniquePointer):
print("take_ptr")
print(p.ptr)
fn work_with_unique_ptrs():
let p = UniquePointer(100)
use_ptr(p) # Perfectly fine to pass to borrowing function.
use_ptr(p)
take_ptr(p^) # Pass ownership of the `p` value to another function.
work_with_unique_ptrs()
Because it is declared owned, the take_ptr function knows it has unique access to the value. This
is very important for things like unique pointers, can be useful to avoid copies, and is a
generalization for other cases as well.
For example, you will notably see the owned convention on destructors and on consuming move
initializers, e.g., our HeapArray used that in its __del__ method - this is because you need to own
a value to destroy it or to steal its parts!
This is because you need to own a value to destroy it or to steal its parts!
@register_passable struct decorator
As described above, the default fundamental model for working with values is that they live in
memory so they have identity, which means they are passed indirectly to and from functions
(equivalently, they are passed ‘by reference’ at the machine level). This is great for types that
cannot be moved, and is a good safe default for large objects or things with expensive copy
operations. However, it is really inefficient for tiny things like a single integer or floating point
number!
To solve this, Mojo allows structs to opt-in to being passed in a register instead of passing through
memory with the @register_passable decorator. You’ll see this decorator on types like Int in the
standard library:
[ ]:
@register_passable("trivial")
struct MyInt:
var value: Int
let x = MyInt(10)
The basic @register_passable decorator does not change the fundamental behavior of a type: it
still needs to have a __copyinit__ method to be copyable, may still
have __init__ and __del__ methods, etc. The major effect of this decorator is on internal
implementation details: @register_passable types are typically passed in machine registers
(subject to the details of the underlying architecture of course).
There are only a few observable effects of this decorator to the typical Mojo programmer:
1.
@register_passable types are not being able to hold instances of types that are not
themselves @register_passable.
2.
3.
instances of @register_passable types do not have predictable identity, and so the ‘self’
pointer is not stable/predictable (e.g. in hash tables).
4.
5.
@register_passable arguments and result are exposed to C and C++ directly, instead of
being passed by-pointer.
6.
7.
The __init__ and __copyinit__ methods of this type are implicitly static (like __new__ in
Python) and return its result by-value instead of taking inout self.
8.
We expect that this decorator will be used pervasively on core standard library types, but is safe to
ignore for general application level code.
@always_inline decorator
For implementing high-performant kernels it's often important to control optimizations that
compiler applies to the code. It's important to be able to both enable optimizations we need and
disable optimizations we do not want. Traditional compilers usually rely on various heuristics to
decide whether to apply a given optimization or not (e.g. whether to inline a call or not, or whether
to unroll a loop or not). While this usually gives a decent baseline, it's often unpredictable. That's
why Mojo introduces special decorators that provide full control over compiler optimizations.
The first decorator we'll demonstrate is @always_inline. It is used on a function and instructs
compiler to always inline this function when it's called.
[ ]:
@always_inline
fn foo(x: Int, y: Int) -> Int:
return x + y
fn bar(z: Int):
let r = foo(z, z) # This call will be inlined
In future we will also introduce an opposite decorator, which would prevent compiler from
inlining a function, and similar decorators to control other optimizations, such as loop unrolling.
This feature is used, for instance, to back our SIMD type implementation. If you'd like to learn
more about it, you can take a look at the Low-Level IR notebook that gives a taste of it.