Lesson 2: Common programming concepts.
đĻ Variables
A variable is a value that has an arbitrary name, defined by a declaration.
In Motoko, variables can be declared using either the let
or var
keyword, followed by the assignment operator =
- Variables declared with
let
are immutable, meaning that their value cannot be changed once they are assigned.
let n = 1;
- Variables declared with
var
are mutable, their value can be reassigned to a new value at any time using the reassignment operator:=
.
var n = 1;
n := 2;
The syntax convention is to use lowerCamelCase
for variable names and to use spaces around the =
sign. Also, a variable declaration ends with a semicolon ;
Whenever you declare a variable don't forget to end the declaration with ;
otherwise Motoko will complain.
If we try the following code:
let n = 1;
n := 2;
An attempt is made to reassign a value to an immutable variable - that's why an error will occur. The specific error message will be type error [M0073], expected mutable assignment target
. This message indicates that the variable being reassigned is immutable and cannot be changed.
đ Types
The Motoko language places a strong emphasis on types and is more strict in enforcing them compared to other general-purpose languages like JavaScript or Python. This strictness serves a purpose, as it helps prevent errors and issues.
Motoko has static types, this means that each variable is assigned a specific type, which is determined before the program runs. The compiler checks each use of the variable to avoid errors that may occur during runtime.
To assign a type to a variable we use the :
symbol, this is called type annotation.
let age : Nat = 20;
let message : Text = "Of all the acts, the most complete is that of building";
let open : Bool = false;
You can generally omit the type declaration - the Motoko compiler will automatically guess the type to the variable based on the first value that you provide this is called type inference.
let age = 20; // Will be assigned type Nat
For the duration of the Bootcamp it is recommended to keep all type declarations to make things clearer, especially if you are new to typed languages.
đŦ Comments.
A one-line comment is written by starting the line with //
.
// Hello! It is recommended to use comments to make your code more readable.
A comment can span into multiple lines, in that case you'll need to add //
on each line.
// Sometimes you'll have a lot to say
// In those cases
// You can use more than one line
âī¸ Functions
This section focuses solely on functions that are defined within the body of an actor using the actor {}
syntax. Any function that is outside the actor {}
syntax will be covered in future lessons.
A simple example
To introduce functions - let's look at an example: here is an actor that is responsible to keep track of a counter.
actor Counter {
var count : Nat = 0;
public func setCount(n : Nat) : async Nat {
count := n;
return count;
};
};
The keyword func
is used to declare functions, followed by the name given to the function, in that case increaseCount
. As for variables, the syntax convention is to use lowerCamelCase
for function names.
Function type
When defining a function in Motoko, the typed arguments and return type are used to determine the type of the function as a whole. For example, the function setCount has the following type:
setCount : (n : Nat) -> async Nat;
To declare a function in Motoko, you must specify the types of the arguments and return values. Function arguments are enclosed in parentheses ()
, and in this case, the function takes an argument n
of type Nat
. After the function arguments, the return type is specified : async Nat
.
You might be wondering why the return type for the
setCount
function isasync Nat
instead of simplyNat
?
The termasync
stands for asynchronous, which means that in the Actor model we discussed earlier, canisters or actors communicate with each other asynchronously. When one canister sends a request to another (or when a user calls a function), there will be a brief waiting period before the caller receives a response.
Asynchronous programming allows you to run your code in a non-blocking manner. Theasync Nat
return type for thesetCount
function indicates that the caller must wait for a few moments before receiving the return value. Eventually, the response will be a value of typeNat
, but with a delay due to the asynchronous nature of the communication between canisters. All public functions declared in the body of an actor must be of return typeasync
.
Body and return
- The curly brackets
{}
are used for the function body. The body of the function is a set of instructions executed when the function is being called. In our example, forsetCount
we have 2 instructions:
- Assign the value of
count
to the value ofn
. - Return the current value of
count
.
Motoko allows the return
at the end of the body of a function to be omitted, because a block always evaluates to its last expression. Which means, we could rewrite the code in the following way and it would still be valid:
public func setCount(n : Nat) : async Nat {
count := n;
count;
};
Public vs Private
So far we've only seen public
functions. However, in Motoko you can also define private
functions.
private func add(n : Nat, m : Nat) : Nat {
return (n + m)
};
The function is now marked private
, this means that it can only be used by the actor himself and cannot be called directly by users or external canisters.
Usually private
functions are used as helpers in other functions, that are generally defined as public
. For instance we could write the following.
actor {
var count : Nat = 0;
private func add(n : Nat, m : Nat) : Nat {
return (n + m)
};
public func addCount(n : Nat) : async Nat {
let newCount = add(count,n);
count := newCount;
return count;
};
}
We can remove the
private
keyword , a function declaration defaults to a private function in Motoko unless declared otherwise (i.e unless declaredpublic
).
đšī¸ Control flow.
Control flow refers to the order in which a program is executed and the order that it follows. It decides which statements, instructions or function calls are executed and in what order, based on conditions or decisions made during the run time.
We discuss three common control flow constructs in Motoko: if else
expressions, loops
expressions and switch
expressions.
If/else
The if
statement allows the program to make a decision and execute a certain block of code only if a specific condition is met. The optional else
statement provides an alternative if the condition is not met.
func isEven(n : Nat) : Bool {
if(n % 2 == 0){
return true
} else {
return false
};
};
In this case, the condition n % 2
will be tested at runtime and depending on the value of n
will returns true
or false
.
In many cases the else
block can be removed without modyfing the behavior of the code block.
func isEven(n : Nat) : Bool {
if(n % 2 == 0){
return true;
};
return false;
};
In other cases, you can add else if
blocks to check additional conditions.
func checkNumber(i : Int) : Text {
if(n < 0) {
return ("The number is negative.");
} else if (n == 0) {
return("The number is zero.");
} else if (n < 10) {
return("The number is one digits.");
} else if (n < 100) {
return("The number is two digits.");
} else {
return ("The number is three or more digits.");
}
};
Note that else if
statements are used after the initial if
statement to check additional conditions, and only the code block associated with the first condition that evaluates to true will be executed.
Loops
Loops enable the repeated execution of a code block until a specific condition is fulfilled. There are various types of loops, such as for
loops and while
loops:
for
loops in Motoko use an iterator of theIter
type. We will delve into theIter
type in a later lesson, but to summarize,Iter
objects facilitate looping through collections of data.
var count : Nat = 0;
for (x in Iter.range(0, 10)) {
count += 1;
};
In this example, Iter.range(0, 10)
iterates through all natural numbers between 0 and 10, inclusive of both boundaries.
Alternatively, you can use while
loops, which executes as long as the specified conditions remains true.
var count : Nat = 0;
while (count < 10) {
count += 1;
};
Here, the loop will continue to execute until the count
variable is no longer less than 10.
Switch/case
The switch
expression in Motoko is a control flow construct that matches patterns based on its input. It begins with the switch
keyword, followed by the input expression enclosed in parentheses ()
, and a code block enclosed in curly braces {}
.
let x = 3;
switch(x) {
//
};
Within the code block, the case
keyword is used to define patterns and expressions enclosed in curly braces {}
. The input is compared to the patterns specified in each case, and if a match is found, the expression within the corresponding case
block is executed.
let x = 3;
switch(x) {
case(0) {
// This corresponds to the case x == 0
return ("x is equal to 0");
};
case (1) {
// This corresponds to the case x == 1
return ("x is equal to 1");
};
case (2) {
// This corresponds to the case x == 2
return ("x is equal to 2");
};
case (_) {
// This corresponds to all other cases
return ("x is above 2");
};
};
In Motoko, switch
expression must cover every possible outcome to ensure the code is valid. When we don't want to list all possible values we can use the special case(_)
to match any value. By putting it at the end of our code it will match all the possible cases that arent specified before it. The underscore symbol (_
) is a wildcard that matches any value, so the case(_)
pattern will match any input value.
The switch/case
expression is best used with variants.
type Day = {
#Monday;
#Tuesday;
#Wednesday;
#Thursday;
#Friday;
#Saturday;
#Sunday;
};
let day = #Monday;
switch(day) {
case(#Monday){
return ("Today is Monday");
};
case(#Tuesday){
return ("Today is Tuesday");
};
case(#Wednesday){
return ("Today is Wednesday");
};
case(#Thursday){
return ("Today is Thursday");
};
case(#Friday){
return ("Today is Friday");
};
case(#Saturday){
return ("Today is Saturday");
};
case(#Sunday){
return ("Enjoy your Sunday");
};
};
In this example, we defined a variant type Day
, declared a variable day
with that type, and then used it as input in our switch
expression. The switch expression is a powerful control flow construct that allows for pattern matching, providing a concise and readable way to handle multiple cases based on the input value.