4. Constants and variables

Sometimes you need to store a value before it can be used in a function. Mojo offers two ways to store a value for later use. One is the concept called a constant and the other one is variable.

4.1. Constants

Suppose you have two functions, one that calculates circumference of a circle and the other that calculates the area of a circle.

def circumference(r):
    return 2 * 3.14 * r

def area(r):
    return 3.14 * r * r

def main():
    print(circumference(25))
    print(area(25))

In the above example, the PI has been limited to 2 decimal places. What if we want to increase our precision and increase the PI to 4 decimal places? Then we need to change both the functions circumference and area.

An alternative is to define PI as a constant using the keyword alias.

alias PI = 3.14

def circumference(r):
    return 2 * PI * r

def area(r):
    return PI * r * r

def main():
    print(circumference(25))
    print(area(25))

The result of both the programs are the same. However, we have now defined PI in one single place and we can change its value in just one place and the updated value is reflected wherever the constant PI is referred.

The main benefit of a constant is that the compiler prevents any attempt to change the initially assigned value during the program execution.

fn main():
    alias counter: Int = 1
    print(counter)
    counter = counter + 3
    print(counter)

For example, executing the above program results in:

error: expression must be mutable in assignment
    counter = counter + 3
    ^~~~~~~
mojo: error: failed to parse the provided Mojo

4.2. Variables

In contrast to the constants, values stored within variables are expected to change during the program execution. An example could be a counter which gets updated each time a user clicks a button.

fn main():
    var counter: Int = 1
    print(counter)
    counter = counter + 3
    print(counter)

If you execute the above program, you would see the result:

1
4

The statement var counter: Int = 1 assigned the value 1 to the variable called counter. The keyword var declares that counter is a variable. The statement counter = counter + 3 adds value 3 to counter, resulting in value 4.

You do not always need to initialize a variable with value. You can also just write var counter: Int and initialize its value later when needed. However, ensure that you initialize a variable before first use of that variable, otherwise you will encounter a compilation error.

fn main():
    var counter: Int
    print(counter)
    counter = counter + 3
    print(counter)

Results in:

error: use of uninitialized value 'counter'
    print(counter)
          ^

The solution to above is to assign a value to counter before printing it.

4.3. Alias

The alias feature provided by Mojo is much more powerful than just declaring constants. In fact, it enforces compile time execution of statements that come on the right hand side of the = symbol.

alias MY_VALUE = add(2, 3)

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

fn main():
    print(MY_VALUE)

In the code sample shown above, you see that Mojo allows compile time execution of a normal function. Executing the program results in the printing of the value 5 on screen.

There are some restrictions on what type of functions can be called during compile time. For example, def style functions cannot be called during compile time. Another restriction is that function that are called during compile must not have any side effects. That is, the functions must use only the arguments passed to it and must not change any variables or state outside of the function body. These kind of functions are called pure functions.

You can also assign types to alias as shown in the next example.

alias MyInt = Int

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

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

In the previous example, the MyInt is exactly the same as Int, just with a different name.

4.4. Undeclared variables

Mojo allows variables to be defined without the var keyword. However, that works only within def functions. fn functions are strict regarding declaration of variables and require var keyword.

def main():
    counter = 1
    print(counter)
    counter = counter + 3
    print(counter)

4.5. Variable scoping

Scoping of a variable means which part of the program sees what value of the variable, and whether or not the variable is even valid at that location. In Mojo functions, a variable can have either a lexical scope or the function scope.

def main():
    x = 1
    y = 1
    if True:
        x = 4
        print("inner x:", x)
        var y = 4
        print("inner y:", y)
    print("outer x:", x)
    print("outer y:", y)

When you execute the code shown above (please ignore the warnings for the moment), you see:

inner x: 4
inner y: 4
outer x: 4
outer y: 1

If you look carefully, the y declared inside the if block had two different values when printed. The inner block had value 4 and the outer block preserved its original value of 1. The x variable on the other hand got overwritten by newer value 4. What happened is that the inner scope from the if block declared a new variable using var keyword. This shadowed the outer variable declaration, resulting in inner scoped variable being different from the outer one, even though the name of the variable was the same. In case of x, no such re-declaration happened, so the scope of x was for the whole function. The var declaration caused y to be lexically scoped within the if block.

4.6. Non-standard identifiers

As mentioned in the beginning of the book, Mojo is a superset of Python. However, there are Mojo keywords that are valid Python identifiers. Since Mojo aims to be compatible with Python, Mojo allows for such variables to be defined within backticks ``. In fact, any kind of text could be used as a variable or function name in Mojo if it is within backticks.

fn main():
    var `var` : Int = 1
    var `with space`: Int = 2

    fn `with#symbol`() -> Int:
        return 3

    print(`var`)
    print(`with space`)
    print(`with#symbol`())