DEV Community

Cover image for Rust - Basics and Concepts, Step by Step — 1st Iteration
Satyajiijt Roy
Satyajiijt Roy

Posted on

Rust - Basics and Concepts, Step by Step — 1st Iteration

For sometime I have been thinking to start coding in Rust however due to never ending things, I couldn’t start. Usually I start learning a language by start writing some utility code in it and learn along the way. However this time, time seems to be not on my side.

So, I decided to start learning small concepts step by step, one or two things at a time and then take it from there. This blog is all about how I took this challenge and started making progress toward learning Rust

Variables a immutable by default

Took me sometime to realize that Rust by default treats all assigned variables as immutable. Which means once binding is done, (another Rust concept, solely means that value has been assigned to the variable), you can’t reassign that variable with different value

    fn main() {
        // Variables by default are immutable use `mut` key word to make
        // mutable
        concept1()
    }

    fn concept1() {
        let x: i8 = 15;
        println!("Value of X = {}", x);
        x = 20;
        println!("After Reassignment, value of X = {}", x);
    }
Enter fullscreen mode Exit fullscreen mode

This doesn’t work

    error[E0384]: cannot assign twice to immutable variable x
      --> src/main.rs:17:5
       |
    15 |     let x: i8 = 15;
       |         -
       |         |
       |         first assignment to x
       |         help: consider making this binding mutable: mut x
    16 |     println!("Value of X = {}", x);
    17 |     x = 20;
       |     **^^^^^^ cannot assign twice to immutable variable**

    For more information about this error, try rustc --explain E0384.
Enter fullscreen mode Exit fullscreen mode

So, question comes in how do we then run counters ? Simple answer is make the variable mutable

    fn main() {
        concept1() // Variables by default are immutable, use `mut`
    }

    fn concept1() {
        let **mut** x: i8 = 15;
        println!("Value of X = {}", x);
        x = 20;
        println!("After Reassignment, value of X = {}", x);
    }
Enter fullscreen mode Exit fullscreen mode

And it runs fine

    Value of X = 15
    After Reassignment, value of X = 20
Enter fullscreen mode Exit fullscreen mode

Why Rust does that, even though Shadowing is possible. In fact Shadowing allows us to change type of the variable as well. Basically in nutshell by using let keyword we are creating an another variable with the same name.

Shadowing a Variable

    fn main() {
        variables_are_immutable();
        variable_shadowing()
    }

    fn variables_are_immutable() { .... }

    fn variable_shadowing() {
        let x: i8 = 15;
        println!("Value of X = {}", x);
        **let** x = x + 20; **// This is possible because of `let` keyword**
        println!("After Reassignment, value of X = {}", x);
    }
Enter fullscreen mode Exit fullscreen mode

And we get

    Value of X = 15
    After Reassignment, value of X = 35
Enter fullscreen mode Exit fullscreen mode

Check out the below illustration for shadowing and code

Shadowing Variables in Rust

    value of shadowed_variable is 10 -> 1st Value
    value of shadowed_variable is ten -> 2nd Value
    value of shadowed_variable is 10 -> First Value in different scope
    value of shadowed_variable is scope check -> 2nd Value different scope
    value of shadowed_variable is 20 -> 3rd Value
    First value of shadowed_variable is 10 -> 1st Value accessed using reference variable first_value
Enter fullscreen mode Exit fullscreen mode

As you can see Shadowing works like a STACK internally and you can remove the previous value by using drop(shadowed_variable) and it will behave like STACK and start poping out values in lifo manner. Also keep in mind that Shadowing variable only work if the variable is in scope.

Similarly const is used to defined constants, which are valid for whole run of the program. Value of the constant can’t be something which needs to calculated at runtime

    const DAY_IN_YEAR: i32 = 31 * 7 + 30 * 4 + 28;

    fn main() {
        print_constant();
    }

    fn variables_are_immutable() {...}

    fn variable_shadowing() -> i32 {
        let x: i32 = 15;
        println!("Value of X = {}", x);
        let x = x + 20;
        println!("After Reassignment, value of X = {}", x);
        return x
    }

    fn print_constant() {
        println!("Value of constant {}", *DAY_IN_YEAR*);
    }
Enter fullscreen mode Exit fullscreen mode

Variable Type Conversion aka Casting

Rust allows to variable type conversion aka Casting on the fly. Like

    fn calculate_avg_using_casting() {
        let (a, b, c): (i32, f32, f32) = (13, 2.3, 120.0);
        let avg = (a as f32 + b + c) / 3.0; // a is re-casted as f32

        assert_eq!(avg, 45.100002);
        println!("Test Passed");
    }
Enter fullscreen mode Exit fullscreen mode

It will print Test Passed . As you can see that variable a has be casted as f32 from i32 using as keyword. Word of caution, casting should be used with care as it might produce unpredictable outcome, like if f32 has been re-casted as i32 then everything after decimal will be truncated. Casting uses truncation then round will type conversion.

The Magic of Functions

Like most of the languages Rust also requires at least one function arguable main() and in Rust we define functions with fn keyword. Apart from this functions in Rust is pretty straight forward.

    fn main() {
        println!("{}", variable_shadowing_and_return());
    }

    fn variable_shadowing_and_return() -> i32 {
        let x: i32 = 15;
        println!("Value of X = {}", x);
        let x = x + 20;
        x * x **// missing `;` is intentional as it is an expression **
    }
Enter fullscreen mode Exit fullscreen mode

Statements vs Expressions

Above is an example of function in Rust with a return type i32 . Functions are combination of Expressions and Statements. Expressions always generates a value and Statements are just an action. So, in Rust something like this is invalid a = b = 1 as we can’t assign a Statements to variable as b = 1 is an action not a value. Adding a ; to Expressions will convert that to Statements

In Rust if the last line is an expressions then it will be passed out as return values for the function.

Function main() can return a Result type from Rust 1.26 onwards (details). For Example

    use std::num::ParseIntError;

    fn main() -> Result<(), ParseIntError> {
        let some_number = "10";
        let n = match some_number.parse::<i32>() {
            Ok(n)  => n,
            Err(e) => return Err(e),
        };
        println!("{}", n);
        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Lastly, if your function doesn’t return any value then it return something called unit data type (). Rust complier automatically infer to unit data type if the function is not returning. We can explicitly mention that too like

    fn some_func(x: i8) **-> ()** {
      println!("Square of X is {}", x * x)
    }
Enter fullscreen mode Exit fullscreen mode

String Literals or Types … hmm!!

Take a look at the below Diff View and I will try to explain what I meant to show

Strings Stack and Heap

In Rust there 2 ways work with Strings using String Literals or String Type. When say let some_string = "actually it is random string" this is called String Literals because it is literally written in the executable. As mentioned above they are immutable and the size needs to be known before compilation. So it can’t hold any value which is dynamic in nature.

A String is stored as a vector of bytes (Vec<u8>), but guaranteed to always be a valid UTF-8 sequence. String is heap allocated, growable and not null terminated. &str is a slice (&[u8]) that always points to a valid UTF-8 sequence, and can be used to view into a String, just like &[T] is a view into Vec<T>.

To deal with dynamic nature of String we use String Type, which are stored in heap and mutable.

The code below will create a variable called my_string which is &str type stored in STACK and will store the value in Heap. In Stack it will hold the pointer to the Heap location, length of the string and capacity. The star suggests that the capacity is always greater than equal to the length of the string.

    fn main() {
        work_with_strings();
    }

    fn work_with_strings() {
        let mut my_string = String::from("APPLE");
        println!("Original my string: {}", my_string);
    }
Enter fullscreen mode Exit fullscreen mode

Strings Stack and Heap

Same code with adding more to the existing string

    fn main() {
        work_with_strings();
    }

    fn work_with_strings() {
        let mut my_string = String::*from*("APPLE");
        println!("Original my string: {}", my_string);
        my_string.push_str(" is GOOD");
        println!("Final my string: {}", my_string);
    }
Enter fullscreen mode Exit fullscreen mode

Becomes something like this

Strings Concatination

To reference characters in string using index, because strings are valid UTF-8, they do not support indexing. We need to use the .chars() method. Read more about the Strings here

    fn main() {
        work_with_strings();
    }

    fn work_with_strings() {
        let mut my_string = String::*from*("APPLE");
        println!("Original my string: {}", my_string);
        my_string.push_str(" is GOOD");
        println!("Final my string: {}", my_string);
        println!("The characters are \n");
        for ch in my_string.chars() {
            print!("{},", ch)
        }
    }
Enter fullscreen mode Exit fullscreen mode

Loopy Loops

Like any other language Rust also has couple of mechanisms which we can use to iterate over some stuff. Loops, While and For, as the name suggest they more or less similar to other language except Loops.

Loops are very much similar to While, with 2 main difference. It doesn’t come with a condition like while and it can return value which while can’t.

Loops vs While

As you can see the loops actually returns the count to the result variable once the break statement executes, however while just execute and breaks out of the loop when the condition matched.

Also notice that because the loop is assigned to a variable result so we put ; at the end of it to make it statement.

Suppose we have nested loops and want to break out to the outer loop. Then, we can use loop labels to specify which loop a break or continue applies to. In the following example, 'outer is the label given to the outer loop.

    'outer: for x in 0..5 {
        for y in 0..5 {
            if y > 2{
                break 'outer
            }
            println!("x: {}, y: {}", x, y);
        }
    }
Enter fullscreen mode Exit fullscreen mode

I think I have enough content to start with for now. Hope this helps a little more to understand the basics of Rust . I am still learning and will keep putting my thoughts in future write-ups!!

Happy Programming!!

Top comments (0)