3. Functions

In the previous chapter you saw how a function print is being called in a program. In order to call a function, the function must be defined somewhere. In Mojo, there are two ways to define a function. The first way is inspired by Rust and the second by Python. Both styles are equally first class in Mojo and both have their own use cases.

3.1. def

def my_function(text: String):
    var name = input("Enter your name: ")
    print(text + name)

def main(): my_function("Hello, ")

In the above code, def my_function(text: String): defined a function named my_function and declared that it takes an argument text of type String. The word that appears after : is called a type annotation. This declaration says that the function argument text will contain only Strings. More importantly, it will never contain any other content than Strings. When we tell Mojo compiler that the text will take only `String`s, it can generate a very specific and highly optimized code that handles only `String`s.

The following statement my_function("Hello World!") calls that function my_function and passes "Hello World!" as the argument where text was declared.

You may have noticed that after def my_function(text: String):, there is a new line and 4 spaces before the statements var name = input("Enter your name: ") and print(text + name). This is because Mojo, like Python, uses whitespace indentation to demarcate the function’s body. Many mainstream programming languages use braces "{}" for function body. However, Mojo uses indented whitespace and is particular about the whitespace being aligned. You will find this syntax not only for function bodies, but also for other statements that expect a block. We will come to those cases in later chapters. Please notice that the statement my_function("Hello World!") appears differently aligned than print(text + name). This is because my_function("Hello World!") is not part of the `my_function’s body itself.

The following illustration shows the simplified structure of a Mojo function.

Function Structure Def

3.2. fn

The other way to define is function is by using the keyword fn.

fn my_function(text: String) raises:
    var name = input("Enter your name: ")
    print(text + name)

fn main() raises: my_function("Hello, ")

The body of the fn functions are also demarcated by whitespace similar to def functions. One main difference is that in def functions, we can raise errors without declaring them. However, in fn functions, we must declare all errors that the function may raise. In the above example, we declared that my_function may raise an error with the keyword raises. Why do we need to declare the errors that a function may raise? The answer is that the input function may raise an error if the input operation fails. Since my_function calls input, it may also raise that error. By declaring the error, we are telling the Mojo compiler that we are aware of the error. If the raise keyword is omitted, the Mojo compiler will complain that we have not handled the error that may be raised by input. We will come back to error handling in detail in later chapters.

The following illustration shows the simplified structure of a fn function.

Function Structure Fn

You may have noticed one additional concept that we did not yet touch upon. The return type. Both fn and def functions support returning values from the function. So far we have not used it. However, in many cases we want to call functions to perform calculations and after the calculations are completed, we expect a result as output from the function. The following examples show such a use case in both def and fn forms.

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

def main(): print(add(1, 2))
fn add(a: Int, b: Int) -> Int:
    return a + b

fn main(): print(add(1, 2))

If you run the above program, you get the output as 3.

The function add took two Int arguments and performed an addition. Then it returned that result with keyword return, which was then shown on the terminal using the print function. The return value of the function is of type Int, which was explicitely delcared with → Int declaration. The Int type represents all integer values, including both positive and negative values. The actual value is passed during the function call statement (call site).

The statement print(add(1, 2)) actually made two function calls. The inner function call add got executed first and then the outer function call print. This is called the nesting of function calls. The result from the inner function call was passed to the outer function call.

mojo addition_return_def.mojo
3
mojo addition_return_fn.mojo
3

3.3. Benefits of fn over def

As mentioned earlier, one of the benefits of using fn is to provide information to the compiler that would be used by the compiler to produce a highly performance code. The second benefit is to enforce program correctness.

Let’s look into the following program.

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

def main(): print(add(1, 1.5))

When you execute the above program, you would see the result as 2.5. Behind the scenes, the expression add(1, 1.5) added two different types. The first argument was of type Int and the second argument was of type Float16. The addition’s result is of type Float16.

FloatLiteral vs Float

I am glossing over when I say that 1.5 is a Float16. Since it is provided in the source code directly, its actual type is FloatLiteral which gets converted to Float16 at runtime. FloatLiteral is a special type that represents all floating point literals in the source code. It can be converted to any of the floating point types like Float16, Float32, Float64 or Float128 depending upon the context. We will come back to this in detail in later chapters.

Mojo ensures that we can pass values to functions that can be converted to the declared type of the function argument. In this case, 1.5 can be converted to Float16, so the program works fine. If we pass a Float to a parameter of type Int, then the compiler would complain that Float cannot be converted to Int.

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

fn main(): print(add(1, 2.5))

If you now execute the above program, you would get a compiler error (you may have slightly different error message depending upon the version of the Mojo compiler used).

addition_return_fn_2.mojo:4:21: error: invalid call to 'add': argument #1 cannot be converted from 'FloatLiteral' to 'Int'

The Mojo compiler complains because we have strictly restricted the argument type of b to Int and Mojo compiler cannot convert the passed value to Int. You have a guarantee from the Mojo compiler that only those values that can be converted safely into Int will be allowed to be passed. This is known as type safety.

3.4. Default return types

In Mojo, when you omit the return type, the function defaults to return None, which indicates that there is no value being returned.

3.5. main function

There is a special function called main in Mojo, which is the function that is used by the compiler to determine the main entrance of a program. When you call a program executable created by Mojo, the very first function that is executed is the main function.

def main():
    print("Hello World!")

If you execute the above program, the main function will be called automatically and the text Hello World! will be printed.

3.6. Different styles of writing functions

A function can be defined in different styles.

def func1(r: Int): ...

def func2(): pass

def func3(): print("Hello World!"); print("Good bye!")

def func4(): 
    pass

def main(): ...

Note that all the above style is also valid for fn functions. You can define the function body in the same line as the function definition only if the whole body is just a single line.

3.6.1. Semicolon

In the source code shown above, you may have noticed a semicolon in the body of function func3. Semicolon can be used to separate statements, which allow the statements to be written in a single line. Mojo follows the philosophy of Python, so use semicolon sparingly and only when it improves reading and understanding of code.

3.6.2. Ellipses in functions

In Mojo you would often see …​ defined in the function body. Ellipsis is just a built-in constant in Mojo, and is a placeholder. Within a function, it just means that the body is not yet implemented and the Mojo compiler will not complain about the missing body. Ellipses have other uses and we will cover them in subsequent chapters.

3.6.3. pass in functions

The pass keyword has a similar role as …​ in functions. It particularly tells the compiler that the implementation has been omitted.

One good rule of thumb is to use pass where you know that there is no need for an implementation and use …​ when you are expecting some implementation in the future (or in inherited entities - we will come to that later).

3.7. Arguments passed to functions

Arguments passed to a function cannot be modified within the function. Such arguments are said to be immutable as the function body cannot modify their value. This kind of restriction is helpful in large programs as the code that calls the function does not get surprised that the value it passed to a function has suddenly changed unexpectedly.

fn add(a: Int, b: Int) -> Int:
    a = a + 1
    return a + b

fn main(): print(add(1, 2))

Executing the above listed code results in compilation error.

error: expression must be mutable in assignment
    a = a + 1
    ^
mojo: error: failed to parse the provided Mojo

3.8. Nested functions

Mojo supports nesting of function within other functions. This applies to both fn and def style functions.

Nesting of functions limit the scope of the nested function to just the outer function. For example in the following code listing, it is not possible to call nested within main.

fn outer():
    fn nested():
        print("I am nested")
    nested()

fn main():
    outer()

3.9. Keyword arguments

Sometimes when functions take lots of arguments, it is much more clear if the name of the argument is specified when calling the function. Mojo supports keyword arguments, which is basically the ability to specify the argument name when we assign a value to that argument during a function call.

fn my_function(first: Int, second: Int) -> Int:
    return first + second

fn main():
    print(my_function(first = 1, second = 2))
    print(my_function(second = 2, first = 3))
    print(my_function(4, second = 5))

In the previous listing, we can see there are three different ways to call the function with keyword arguments. The first one specifies the name of the both arguments when passing the value. The second call demonstrates that when using the keyword arguments, the order of the arguments does not matter, as Mojo knows with the name itself which argument gets which value. In the third call, we see that we can mix and match positional argument with the keyword argument; however, here the order is important as the positional values must appear in the order in which they were declared in the function definition. Keyword arguments follow positional arguments.

What if you as an API designer want some arguments to be always specified positionally? In this case, you can enforce positional arguments by using a special argument, /.

fn my_function2(first: Int, second: Int, /) -> Int:
    return first + second

fn main():
    print(my_function2(first=1, second=2)) # compiler error

Executing the above listed code results in compilation error.

error: invalid call to 'my_function2': positional-only arguments passed as keyword operands: 'first', 'second'

You can also mix and match position-only arguments with keyword arguments. The / can be the last argument, in which case all the function arguments would be position-only. It cannot be the first argument though.

fn my_function(first: Int, second: Int, /, third: Int) -> Int:
    return first + second + third

fn main():
    print(my_function(1, 2, third=3))
    print(my_function(1, 2, 3))

Here the first two arguments are strictly position-only, while the third can be passed as keyword or positional as desired.

What if you wanted some arguments to be always keyword only? In this case, you can enforce keyword arguments by using the special argument, *.

fn my_function(first: Int, *, second: Int, third: Int) -> Int:
    return first + second + third

fn main():
    print(my_function(1, second=2, third=3))
    #print(my_function(1, 2, 3)) # Uncommenting would result in compiler error.

Similar to /, you can also mix and match keyword-only arguments with positional arguments. The * can be the first argument, in which case all the function arguments would be keyword-only. It cannot be the last argument though.

Keyword arguments make APIs ergonomic, as the programmer does not have to remember in which position what value need to be passed. It improves code readability and maintainability. It also reduces accidental mistakes when programmer wrongly assumes the order of the arguments.

Keyword arguments are applicable for both def and fn forms.

3.10. Default value

Mojo allows assigning default values to function arguments, which means when the caller does not pass a value to the argument, the function will take the given default value. That function argument therefore becomes optional for the caller. The default value must be of the same data type as the declared data type of the argument.

This feature is quite useful when defining ergonomic APIs, providing sensible default values for the caller, making the function easier to use.

fn deft_function(first: Int, second: Int = 10) -> Int:
    return first + second

fn main():
    print(deft_function(1)) # 'second' defaults to '10'
    print(deft_function(1, 2))

Default values are applicable for both def and fn forms.