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 very close to Python. However, there are Mojo keywords that are valid Python identifiers. Since Mojo aims to have a syntax that is very close to 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`())