Skip to content

Functions

Declaration

In Python, functions are declared using the def keyword. For example, the following functions returns the sum of two numbers (with or without type hints.)

python
def mysum(a, b):
    return a + b

def mysum(a: int, b: int) -> int:
    return a + b

In Mojo, functions can either be declared with the def keyword or with the fn keyword.

def keyword

Using the def keyword to declare a function would allow you to enjoy some freedom that Python has. For example, you do not need to declare the types of the arguments and returns. You do not need to use the raises keyword for functions that may raise an error. For example, the following functions are valid in mojo.

mojo
def mysum(a, b):
    return a + b

def mysum(a: Int, b: Int) -> Int:
    return a + b

The def functions in mojo do not always behave in the same manner as those in Python. Finally, these are different programming languages. Nevertheless, declaring functions by def is a good starting point if you are familiar with Python and want to try out Mojo.

Yuhao highly recommends you to use fn always to declare functions, which gives you more control over the functions you are writing.

fn keyword

The fn keyword is unique to Mojo and is not present in Python. It is one of the features that allow Mojo to accomplish low-level programming objectives.

Interestingly, the word fn itself does not look Pythonic. Python usually truncates the words from left, e.g., def. Maybe func is a more Pythonic keyword. Nevertheless, Rust users may find fn friendly.

To declare a function by the fn keyword, you have to explicitly indicate the types of arguments and returns. Failing to indicate types will cause the compilation to fail. For example,

mojo
fn mysum(a: Int, b: Int) -> Int:
    return a + b

Whenever you call the function mysum, the compiler checks whether the types of the values passed into the function match with the types that are indicated in the function. This helps you detect bugs in your code at the compile time or at the coding time (with help of LSP).

Indicating the type of the returned value of function in fn can also help you to take the advantages of implicit variable declaration. Consider the following example, since the returned type of the function mysum is Int, you do not need to explicitly declare the type of the variable c during its initialization.

mojo
fn mysum(a: Int, b: Int) -> Int:
    return a + b

var a: Int = 1
var b: Int = 2
var c = mysum(a, b)  # Equivalent to `var c: Int = mysum(a, b)`

main() function

In mojo, all code except imports has to be wrapped in function blocks, unless you are in the REPL (Read-Eval-Print Loop) mode.

This means that you cannot directly write the following code in your Mojo file.

mojo
# This won't work during compilation.
print(1 + 2)

Instead, you have to put everything in the main() function.

mojo
fn main():
    print(1 + 2)

The main() function tells Mojo to start everything from here. This function may be familiar to you if you have experience with programming languages like C, Java, or Rust. In Python, the main() function is not needed and can be automatically inferred by the interpreter (recall the expression if __name__ == "__main__").

For convenience, I will not always put the code in the fn main() block. If you want to test the code, please remember to put the code in the fn main() block in your mojo file.

Arguments

In mojo, names appeared between the parentheses after the function name are called "arguments". They cannot be called as "parameters" because the word is reserved for other purposes. We will discuss this in other chapters.

Positional arguments

If you call the function without writing the name of the arguments, the values will match to the arguments by their position. See the following example. The value 1 and 3.1415 are passed into the function sumint() without a= and b=. Thus, the values will be matched with the arguments by their position. That is, a=1 and b=3.1415.

mojo
fn sumint(a: Int, b: Float64) -> Int:
    return a + Int(b)

def main():
    var a = sumint(1, 3.1415)
    print(a)

Keyword arguments

You can also call the function and write the name of the arguments, the equal sign, and the value. Then the the values will be matched with the arguments by their names.

See the following example. The argument b can comes before the argument a if you explicitly use their names.

mojo
fn sumint(a: Int, b: Float64) -> Int:
    return a + Int(b)

def main():
    var a = sumint(b=3.1415, a=1)
    print(a)

Variadic arguments

Similar with Python, you can pass in an arbitrary number of arguments into the function using *arg. The only difference is that the arguments must be of the same type.

See the following example. By using * before flts, you can pass in any number of Float64 numbers into the function and get their summation. The values you passed into the function will be stored in a variadic list of floats VariadicList[Float64].

mojo
fn sumfloats(*flts: Float64) -> Float64:
    var s: Float64 = 0
    for i in flts:
        s += i
    return s

def main():
    var a = sumfloats(0.1, 0.2, 0.3)
    print(a)

Keywords for arguments

To determine whether a passed-in value can be modified by the function, Mojo uses several keywords to define the behavior of the arguments.

Although Mojo has ownership system, it is different from Rust's. In Rust, if you pass a value into function, the function will take over the ownership of the value. The value is inaccessible after using the function. In order to use the value in a function without transferring the ownership to it, one could pass a reference or mutable reference of the value into the function, e.g., &a or &mut a.

In Mojo, you can directly pass the value into the function. The function will take a reference (alias) of the value. This reference (alias) will behave the same as the value you passed in. The mutability of the arguments is defined by several keywords, namely, read, mut, owned and out.

Reference

The term "reference" means differently in Mojo compared to Rust. Moreover, the ownership model of Mojo is significantly different from that of Rust's. For more information on "reference", please refer to the page reference).

INFO

The keyword read was named as borrowed before v24.6. The keyword mut was named as inout before v24.6.

Keyword read

If an argument is declared with the keyword read, then a read-only reference of the value is passed into the function. The function can access the value stored at certain address in the memory without a copy [1], but it will not modify the value at the address.

The read-only reference behaves the same as the value it refers to. An example goes as follows.

mojo
fn copyit(read some: List[Int]) -> List[Int]:
    b = some.copy()
    return b

def main():
    var lst = List[Int](1, 2, 3, 4)
    var new_lst = copyit(lst)
    for i in new_lst:
        print(i[])

When you pass the list lst into the function copyit(), Mojo creates read-only, immutable reference (alias) of lst. This variable some points to the same address of lst and behave exactly as lst.

The line b = some.copy() then calls the copy() method of some. This generates a deep copy of the list and bind it to the variable b.

The variable b is then returned by copyit() into main() and is assigned to the variable new_lst.

read as default keyword

In the Mojo programming language, when declaring a function, if no keyword is indicated in front of an argument, then it is defaulted to read. So the following two functions are equivalent.

mojo
fn copyit(read some: List[Int]) -> List[Int]:
    b = some.copy()
    return b

fn copyit(some: List[Int]) -> List[Int]:
    b = some.copy()
    return b

Since the read argument convention is immutable, attempting to change the value of the argument will cause an error at compile time. See the following example:

mojo
fn changeit(read some: List[Int]) -> List[Int]:
    some[0] = 100
    return b
console
error: expression must be mutable in assignment
    some[0] = 100
    ~~~~^~~

keyword mut

The keyword read allows you to pass a mutable reference of the value into the function. The function can then modify the value at its original address. It is similar to the Rust fn foo(a: &mut i32), but keep in mind that the reference in Mojo is more like an alias than a safe pointer, which means a de-referencing is not needed. See the following example:

mojo
from memory import Pointer

fn changeit(mut a: Int8):
    a = 10
    print("Address of the argument `a`: ", String(Pointer.address_of(a)))

fn main():
    var x: Int8 = 5
    print("Value of the variable `x` before change: ", x)
    print("Address of the variable `x`: ", String(Pointer.address_of(x)))
    changeit(x)
    print("Value of the variable `x` after change: ", x)
    print("Address of the variable `x`: ", String(Pointer.address_of(x)))
console
Value of the variable `x` before change:  5
Address of the variable `x`:  0x16b6a8fb0
Address of the argument `a`:  0x16b6a8fb0
Value of the variable `x` after change:  10
Address of the variable `x`:  0x16b6a8fb0

Let's look into the code and see what has happened:

First, you create variable with the name x and type Int8 and assign value 5 to it. Mojo assigns a space in the memory, which is of 1-byte (8-bit) length at the address 16b6a8fb0. The value is 5, so it is stored as 00000100 (binary representation of an integer 5) at the address 16b6a8fb0. See the following illustration.

console
        ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
Value   │         │         │ 00000100│         │         │         │
        ├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
Address │16b6a8fae│16b6a8faf│16b6a8fb0│16b6a8fb1│16b6a8fb2│16b6a8fb3│
        └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘

                             x (Int8)

Next, you pass this value into the function changeit with the mut keyword. Mojo will then create a mutable reference of x, which is named as a . This reference a is an alias of x, pointing to the same address 16b6a8fb0. See the following illustration.

console
                             a (Int8): Mutable reference of x

        ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
Value   │         │         │ 00000100│         │         │         │
        ├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
Address │16b6a8fae│16b6a8faf│16b6a8fb0│16b6a8fb1│16b6a8fb2│16b6a8fb3│
        └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘

                             x (Int8)

Then, you assign a value 10 to the a. Since a is a mutable reference of x, this re-assignment is allowed. The line of code is equivalent to re-assigning the value 10 to x. The new value 00001010 (binary representation of the integer 10) is then stored into the memory location of x at address 16b6a8fb0. Now the updated illustration of the memory goes as follows.

console
        ┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
Value   │         │         │ 00001010│         │         │         │
        ├─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
Address │16b6a8fae│16b6a8faf│16b6a8fb0│16b6a8fb1│16b6a8fb2│16b6a8fb3│
        └─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘

                             x (Int8)

Equivalent code in Rust

The code above, if written in Rust, looks like:

rust
fn changeit(a: &mut i8) {
    *a = 10;
}

fn main() {
    let mut x: i8 = 5;
    changeit(&mut x);
    println!("x = {}", x);
}

Let's compare it again with Mojo.

mojo
fn changeit(mut a: Int8):
    a = 10

fn main():
    var x: Int8 = 5
    changeit(x)
    print("x =", x)

keyword mut and out

Yuhao will explain this part later.


  1. For some small structs, a copy is made. ↩︎

Mojo Miji