Once you’re familiarized with the Rust environment it’s time to get into action. Our goal today is to cover the basics of Rust syntax and some of its resources.

Main function and Prelude
Every executable Rust program should contain a main
function. This function always acts as an entry point for our executables. From my last article, you already know how to compile your classical hello world
program using the Rust compiler directly or using Cargo to build and run your program.
Another detail that you should have noticed from the previous article is that Rust source files have the.rs
extension.
Rust also brings some types into the scope of every program. These types, traits, and functions were judged to be so commonly used that the Rust compiler implicitly imports these symbols for you. This is called the prelude. By allowing you to use this set of common types, Rust reduces the verbosity of their programs.
Some basic formatting
Rust brings some macros related to strings. Today we will explore some of them: println! and format!.
The first one is the println!. This macro prints its output directly to UNIX standard output. In fact, we used this macro in the previous article to write our first program in Rust.
fn main() {
println!("Hello, world!");
}
This will print a text on your console. We can also pass some arguments to println! macro and use placeholders to format output.
fn main() {
// Printing a number using the generic placeholder
println!("{}", 65);
// Arguments passed to println! macro are positional arguments
println!("Brazil {} - {} Germany", 1, 7);
// Arguments also are indexed. For example, to invert the score of that semi-final.
println!("Brazil {1} - {0} Germany", 1, 7);
// We can also use named arguments
println!(
"Hello! my name is {name} and I'm from {country}",
name = "Lucas",
country = "Brazil"
);
// You can use special placeholders to show an specific representation of your data
println!("Hexadecimal: {:x} | Octal: {:o} | Binary: {:b}", 16, 16, 16);
// Rust also checks if you're passing the correct number of arguments
// println!("Lets produce an error: {} {}", "passing just one argument"); // 2 positional arguments, but just 1 provided
// Structs are not printable by default
#[allow(dead_code)]
struct MyStruct(i64);
println!("My struct: {}", MyStruct(10)); // Error
}
The format! macro works in a similar way. But instead of printing on the console, it will return a formatted string for you.
fn main() {
// format! macro
let output = format!(
"Hello! my name is {name} and I'm from {country}",
name = "Lucas",
country = "Brazil"
);
println!("{}", output);
}
Primitive types
Rust brings a good set of primitives. On one hand, we have the following scalar types:
Numeric types
Rust supports signed integers, represented by the following types: i8, i16, i32, i64, i128 and isize.
Unsigned integers are the following: u8, u16, u32, u64, u128 and usize.
For floating-point numbers, Rust provides two types: f32
and f64.
Char
The type char
represents Unicode scalar values like ‘a’ or ‘💖’ and has 4 bytes. In C, for example, a char
value uses 8 bits in most cases.
Bool
bool
either true
or false.
// Rust types
fn main() {
// Bool
let logic: bool = true;
// Numerics
let number: i32 = 10;
let snd_number: f64 = 10.75;
let another_number = 10u16; // You can use suffix annotation to define the type a numeric
// arrays
let inferred_arr = [1, 4, 5, 56.3];
let type_annoted_arr: [i32; 3] = [1, 4, 5];
// Tuples
let inferred_tuple = (1, "H");
let type_annoted_tuple: (u8, char, f64) = (1, "H", 34.598);
}
Arrays and Tuples
Rust also has arrays and tuples as compound types. These types can be used when you need to group a set of values in the same structure. Let’s take a look at tuples.
A tuple can receive a group of values from different types. Tuples in Rust cannot grow or shrink after its declaration. To access directly an element in a tuple using a period (.
) with the index of the element you want.
// Tuples
fn main() {
// Accessing tuple values
let my_tuple: (i32, f64, u8) = (65, 7.65, 1);
let age = x.0;
let money = x.1;
let one = x.2;
// You can also use pattern matching to destructure a tuple value
let (x, y, z) = my_tuple;
println!("The value of y is: {}", y);
}
Arrays in Rust are a little bit different from most languages. First, they have fixed lengths as in C and C++ (Not to be confused with vectors).
// Array
fn main() {
// Declaring an array
let a = [1, 2, 3, 4, 5];
let b: [i64, 3] = [2, 4, 8]; // type annoted
// Access elements by indexes
let first_elem = arr[0];
let snd_elem = arr[1];
// Invalid index
let idx = 10;
let element = a[idx]; // Will produce a runtime error
println!("The value of element is: {}", element);
}
If you, for some reason, try to access a non-existent index on an array, Rust will terminate the program execution. Yep. Rust always checks if the index is greater than or equal to the array length. This is a glimpse of Rust’s safety working. In some languages, this is still a problem and you should pay attention while writing your program to keep everything ok.
Rust also supports custom types like structs and enums which we’ll explore later.
A curious observation about types is that an empty tuple is considered a scalar type, once it represents a simple zero-sized type, with only one value possible: ()
.
Variables
Variables in Rust are declared with let
keyword and all of them are, by default, immutable. You can’t reassign a variable after its creation. Variables in Rust can be type annotated. This means that you explicitly declare the type of variable. However, the compiler is smart enough to infer the type of a variable from context.
// Rust variables
fn main() {
let bool_var = true;
let age: i32 = 49; // type annotation
// Boolean variables can receive their value from an expression
let condition: bool = 10 > 5;
bool_var = false; // Error. You can't reassign to immutable variable
println!("Value: {}", bool_var);
}
But, of course, you can use mutable variables by adding the keyword mut
on the declaration.
// Rust variables
fn main() {
let mut year = 2020;
println!("year: {}", year);
year += 1;
println!("year: {}", year);
}
Scope
Variables always live in a block. In Rust, a block is a collection of statements enclosed by braces.
// Rust variables
fn main() {
let mut year = 2020;
{
// A block defines a variable scope in Rust
// Variable shadowing is possible too.
let year = 2018;
println!("From block scope: {}", year);
}
println!("From outer scope: {}", year);
}
In Rust, you can declare a variable in a certain scope with the same name as a variable from an outer scope. This is called shadowing or masking. The outer variable is shadowed by the inner variable.
// Shadowing
fn main() {
let outer = 100;
{
let outer = 123;
println!("Inner outer: {}", outer)
}
println!("Global outer: {}", outer)
}
When you use a mutable variable in a nested scope, their changes on the inner scope reflect on the outer scope.
fn main() {
// Mutable variable
let mut age = 17i32;
{
age = age + 1;
println!("Inner scope: {}", age); // 18
}
println!("Outer scope: {}", age); // 18
}
But when two variables are bound to the same name in different scopes with an immutable declaration, the variable data freezes until the immutable binding ends its scope.
fn main() {
// Mutable variable
let mut age = 17i32;
{
// Shadowing by immutable `age`
let age = age;
// 'age' is frozen in this scope
age = age + 1; // Error
println!("Inner scope: {}", age);
}
age = age + 5;
println!("Outer scope: {}", age);
}
Casting
Implicit type conversions between primitive types have no space in Rust. You can perform type conversion using theas
keyword. There are a few examples:
fn main() {
// 64-bit float
let fnumber = 389.54647871;
// Error here. Coercion is not allowed in Rust
let intnumber: u32 = fnumber; // comment to compile
// Only explicit conversions are allowed
let intnumber = fnumber as u8;
let chartype = intnumber as char;
// Some conversions cannot be made.
// A float variable can't be converted to a char.
let chartype = fnumber as char; // comment to compile
println!("float -> integer -> char: {} -> {} -> {}", fnumber, intnumber, chartype);
}
Summarizing: Integer types can be cast to float types. Float types can only be cast to integer types. Boolean values can be cast to integers type (where true will equal value one and false will equal 0). char
can be cast to integer types, but only one integer type can be cast to char: u8
;
The type coercions in Rust have other nuances that deserve an article of their own. But for now, this is enough for us to keep moving;
fn main() {
let floatvalue = 45.4545_f32;
let charvalue = floatvalue as char; // Cannot be type casted
// println!("float -> char: {}", charvalue);
let intvalue = floatvalue as u8;
let charvalue = intvalue as char; // Cannot be type casted
println!("float -> char: {}", charvalue);
// Boolean casts
let t = true;
let f = false;
println!("bool -> int: {} {}", t as u8, f as u16);
// Char to integer casts
println!("char -> int: {}", charvalue as i64);
}
Functions
Functions in Rust are defined with the fn
keyword. As you may know, they can have arguments. Functions in Rust are named using snake case. If a function doesn’t follow this convention, the compiler will show you a warning about naming conventions.
// Functions
fn hello_world()
{
println!("Hello world");
}
fn main() {
hello_world();
}
Let’s take a closer look at the anatomy of a function in Rust:
// Functions
fn fibonacci(x: i64) -> i64 {
if x < 2 {
return x;
}
fibonacci(x - 1) + fibonacci(x - 2)
}
fn main() {
println!("Fibonacci for {}: {}", 8, fibonacci(3));
}
You’re probably already familiar with the Fibonacci sequence. Our function above defines a recursive algorithm to calculate the n-th number in the sequence.
We defined a function with the fn
keyword and this function takes an argument called x
type i64
. Parameters are separated by a comma and their types are required. We define the return type declaring it after an arrow (->
). These are the basics of functions in Rust.
// Functions
// Multiple parameters
fn add(x: i64, y: i64) -> i64 {
x + y
}
fn main() {
println!("Result: {}", add(45, 56));
}
Next steps
Today we covered the basics of Rust’s syntax, types, and functions. From now on, we’ll see more action with Rust implementing some data structures and exploring more on arrays and vectors.
You’re free to add comments, ask questions, or make suggestions here. I hope this explanation helped with your understanding of Rust.
See ya!
Leave a Reply