1.2 Motoko level 1
Overview
Motoko is a programming language that has been developed and optimized for creating programs built on the BigFile. In this module, you'll cover the fundamental basics of Motoko that you'll use when you build your first dapp.
Basic concepts and terms
In previous modules, you've been introduced to and briefly discussed actors, and how each cube smart contract is represented by an actor. Within Motoko, the term actor is used to refer to any cube, regardless of the cube's language (Motoko, Rust, etc.). Motoko is designed specifically to make actors easy to write and use once the cube has been deployed.
With actors in mind, the following terms and concepts are essential to be aware of. Many of these concepts apply to several different programming languages, though you'll be focusing on their application within Motoko.
Declaration: A declaration in Motoko is used to define immutable variables, mutable state, objects, actors, classes, and other data types.
Expression: An expression in Motoko is used to describe computations that involve declarations.
Program: A program in Motoko is a collection of expressions and declarations.
Value: In Motoko, a value represents an entity that can be manipulated by a program.
Variable: In Motoko, variables are used to store data or information that can be referenced or manipulated by a program.
Type: A type in Motoko makes a precise prediction regarding the program's result.
Motoko syntax
Motoko's program syntax uses declarations and expressions. Programs consist of an actor expression that is introduced using the keyword actor
. To introduce the Motoko syntax, first you'll use small code snippets that do not define entire programs, but rather some simple printed terminal output.
For example, the following code snippet consists of two declarations for the variables x
and y
, followed by an expression. This forms a single program.
let x = 1; /// declaration
let y = x + 1; /// declaration
x * y + x; /// expression
In this snippet, the program's type is Nat
(natural number), since when the program is run, the result value is a Nat
value of 3. This tutorial series will dive into value types in values.
Now, let's build upon this small snippet. If you introduce a block with enclosing braces do {
and }
and another variable z
, you can amend your original program as follows:
let z = do {
let x = 1;
let y = x + 1;
x * y + x
};
The variable z
now stores the result value of 3
, and can be called by additional methods.
Next, let's look at using and importing the base Motoko library.
Using the base library
Motoko is designed to minimize built-in types and operations, so whenever possible the Motoko base library provides the necessary operations and types for developers to utilize.
The base library includes a selection of modules that focus on the core features of Motoko. The base library is still under development, and is expected to grow in size as additional features are developed.
To import the base library, the import
keyword can be used at the start of your Motoko code file. After the import
keyword, you need to provide a local module name and a file path that the import declaration can use to locate the imported module.
For example, to import a local module named 'Debug', you can use the following import statement:
import Debug "mo:base/Debug";
Then, to use this imported library, you can call the module with a line of code such as:
Debug.print("hello world");
In this demonstration, you import Motoko code, indicated by the mo:
prefix, then specify the base/
path indicating that you are using the base library, followed by the module's file name, Debug.mo
. Note that the .mo
extension is not included in the import statement.
Additionally, you can import Motoko code and other modules using their relative paths. For example, if you have a Motoko program named types.mo
that you'd like to import into your Motoko file, you can use the following import declaration:
import Types "./types";
In this import declaration, you don't need to include the mo:
prefix or the base/
path, since you are importing a module from a local relative path rather than the base library.
Declarations and expressions
A declaration in Motoko is used to introduce immutable variables, mutable state, objects, classes, actors, and other data types. An expression is used to describe computations that involve these declarations.
To demonstrate declarations and expressions, recall our first sample project from above:
let x = 1;
let y = x + 1;
x * y + x;
In this program, there are three declarations:
- Immutable variable
x
, defined via declarationlet x = 1;
. - Immutable variable
y
, defined via declarationlet y = x + 1;
. - An unnamed, implicit variable holding the final expression’s value,
x * y + x
.
The last expression, x * y + x
illustrates a more general principle where expression can be thought of as a declaration where necessary, since the language implicitly declares an unnamed variable with that expression’s result value.
When an expression appears as the final declaration, the expression can have any type. In our example, the expression x * y + x
has type Nat
.
Type Nat
refers to 'Natural numbers', which consist of values such as 0, 1, 2, ...
. Nat
values are not bound to a maximum size; rather, the runtime representation of these values accommodates arbitrary-sized numbers, making their "overflow" nearly impossible.
Motoko permits standard arithmetic operations as one would expect, such as:
let x = 42 + (1 * 37) / 12: Nat
This program results to the value 45, also of type Nat
.
Expressions can also be formed using a block. You can form a block expression from your list of declarations by enclosing it within curly brackets {}
. A block expression is used to preserve the autonomy of the declaration list and the variable's names.
Blocks are only allowed as sub-expressions of control flow expressions. Control flow expressions are programming methods such as if
, loop
, case
, etc. In any other place, you can use do { ... }
to represent block expressions and distinguish blocks from object literals.
So, going back to the example, since there are no control flow expressions in our program, such as if
or loop
, you must use the do { ... }
expression:
do {
let x = 1;
let y = x + 1;
x * y + x
}
Using the do { ... }
expression allows our code to remain a functioning program, but now our declared variables x
and y
are privately scoped within the block expression you have defined.
Next, you can use a block expression to produce a value within a larger, compound expression, such as:
100 +
(do {
let x = 1;
let y = x + 1;
x * y + x
})
You can see that nesting blocks preserves the autonomy of each separate declaration list and its variable names. Language theorists refer to this as lexical scoping, where variables' scopes may nest, but do not interfere with one another as they nest.
To further demonstrate this, consider the following example:
let x = 40; let y = 2;
ignore do {
let x = 1;
let y = x + 1;
x * y + x
};
x + y
In this example, the final expression x + y
has a value of 42
, since it refers to the first declarations of x
and y
, prior to the block expression definition. This is because the block expression's declarations do not interfere with the other declarations within the program.
Aside from program clarity, the brief benefit of lexical scoping is program security, since it can be used to build compositionally-secure systems. Motoko specifically has very strong composition properties, since nesting programs within another program cannot arbitrarily redefine your variables to have other meanings.
Defining an actor
In the previous examples, you've just demonstrated syntax using simple declaration and expression lines; however, to encapsulate our code within a smart contract, you need to define an actor that will house the code.
Recall from our previous modules that an actor is a process with encapsulated state that communicates with other running actors.
To define an actor, the following syntax is used:
actor {
//actor code goes here
}
This actor definition is empty, meaning it does not define any data or send and receive messages.
So, to take our previous example and encapsulate it within an actor definition, our code would be:
actor {
let x = 40; let y = 2;
ignore do {
let x = 1;
let y = x + 1;
x * y + x
};
x + y
}
Values and evaluation
In the previous examples, you explored expressions that produced natural numbers (value type Nat
). There are several other value forms, which you'll dive deeper into now.
Primitive values
Primitive values are predefined by the language and are named using reserved keywords. In Motoko, the following primitive value forms are available:
Boolean values; (
true
andfalse
).Integers:
(…,-2, -1, 0, 1, 2, …)
; bounded (finite) and unbounded (infinite) variants.Natural numbers:
(0, 1, 2, …)
; bounded (finite) and unbounded (infinite) variants.Text values; strings of unicode characters.
Non-primitive values
Additionally, Motoko supports the following user-defined non-primitive value forms and their associated data types:
Tuples; including the unit value (the "empty tuple").
Arrays; with both immutable and mutable variants.
Objects; with named, unordered fields and methods.
Variants; with named constructors and optional payload values.
Function values; including shareable functions.
Async values; also known as promises or futures.
Error values that carry the payload of exceptions and system failures.
Printing values
Earlier, recall that you printed a value using the imported Debug
library:
import Debug "mo:base/Debug";
Debug.print("hello world");
The print
function accepts a text string of type Text
as the input, and returns the unit value of unit
type or ()
as the output. Unit values do not carry information, as all values of unit
type or ()
are identical, therefore the print
function does not return any interesting result. Instead, the print
function is able to return the input text string in a human-readable form to your terminal window; this is known as a 'side effect'.
Functions, such as print
, that have side effects such as modifying state or returning output, are called 'impure' functions. In contrast, 'pure' functions are functions that only return values and do not provide further 'side effects'.
Next, let's look at transforming a Motoko value into a human-readable text string that can be used for program debugging.
The debug_show
primitive value permits converting a large class of values into type Text
values. For example, let's convert three values of type Text
, Nat
, and Text
into human-readable text:
import Debug "mo:base/Debug";
Debug.print(debug_show(("hello", 42, "world")))
This program, when run, will return:
("hello", 42, "world")
This transformation function can be used to print most Motoko data types as text to be used for debugging.
Passing text arguments
To wrap things up, let's take a look at how text arguments can be passed into a function. First, consider the following code example:
actor {
public func location(city : Text) : async Text {
return "Hello, " # city # "!";
};
};
In this example, you define a single actor with the function location
, which accepts a Text
input for the value of city
. Then, it returns the output 'Hello, city
!".
If this example is used in a live cube, the cube can be called and an input text argument can be passed to the function, such as:
dfx cube call location_hello_backend location "San Francisco"
In this example call, there is a space in between 'San' and 'Francisco', so the argument must be enclosed in quotes. The output of this cube call will be:
("Hello, San Francisco!")
Alternatively, if no space is used in the argument, it can be passed without quotes, since Candid will infer the data type as Text
, such as:
dfx cube call location_hello_backend location Paris
The output of this cube call will be:
("Hello, Paris!")
Need help?
Did you get stuck somewhere in this tutorial, or feel like you need additional help understanding some of the concepts? The BIG community has several resources available for developers, like working groups and bootcamps, along with our Discord community, forum, and events such as hackathons. Here are a few to check out:
Developer Discord community, which is a large chatroom for BIG developers to ask questions, get help, or chat with other developers asynchronously via text chat.
Submit your feedback to the BIG Developer feedback board.
Next steps
Now that you've covered the basics of Motoko, let's put these concepts all together in practice by developing your first dapp!