Learning together: Rust – A quick overview of Rust programming language

Originally posted on Medium

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.rsextension.

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.

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.

Rust
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.

Rust
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: f32and f64.

Char

The type charrepresents 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 trueor false.

Rust
// 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.

Rust
// 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).

Rust
// 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 letkeyword 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
// 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
// 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
// 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.

Rust
// 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.

Rust
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.

Rust
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:

Rust
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;

Rust
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 fnkeyword. 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.

Rust
// 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:

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.

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!

Published by


Leave a Reply

Your email address will not be published. Required fields are marked *