Overview

This chapter covers programming basics that are common in most languages. In my case, I’ll likely know most of this but review doesn’t hurt.

Keywords

These are specific words that are reserved for the languages syntax. Do NOT use them as the names of variables in your programs.

Variables and Mutability in Rust

As mentioned in Learning Rust - Ch.2 (Matching, Methods, Crates, and Error handling) variables in Rust are immutable by default, unless defined with the mut keyword. This is a nudge in the direction of memory safety.

Example:

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

This example contains a typical error, after printing x the first time, we try to reassign the value of an immutable. If you run the program, using cargo run, then you’d get the following message in your console:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {x}");
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 5;
  |         +++

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error

That's as beautiful an error message I've ever seen!

To fix this error, we simply define the variable with the mut kw: let mut x = 5;.

Constants

Constants are also values bound to a name that are immutable, but they’re not exactly the same as immutable variables.

Constants are immutable by default and are defined using the const keyword not let, and the type of value must be annotated from the start. We can declare constants in any scope: block, function, and global. Constants may be set only to a constant expression, NOT the result of a value that could only be computed at runtime. Example:

const SECONDS_IN_AN_HOUR: u32 = 60 * 60;

Rust’s naming convention for constants is to use ALL_CAPS_SNAKE_CASE.

When to use them?

  • Defining values that won’t change throughout the run time of the program. E.g max number of something or the speed of light.
  • Immutable values to be used throughout the program multiple times. Well named constants are also a great way to convey meaning of your code to future maintainers.

Shadowing

Shadowing is when a variable has the same name of another, often in a higher scope. In Rust, we can shadow even in the same scope. This means that the second variable is what the compiler will see when you use the name of the variable. Example:

fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }
    println!("The value of x is: {x}");
}

Run the program:

[dev@dev-manjo debug]$ ./variables 
The value of x in the inner scope is: 12
The value of x is: 6

Shadowing is different from reassigning, it effectively creates a new variable with that name because it uses the let kw.

Example:

    let mut spaces = "   ";
    spaces = spaces.len();

This is broken and doesn’t work because we’re trying to mutate the spaces var. Another example:

    let spaces = "   ";
    let spaces = spaces.len();

This works because it’s a new var, as demonstrated by the difference in data type.

Data Types

Rust is statically typed but also has type inference, this means the compiler can figure out the type if we don’t specify it. This does NOT mean we can reassign different types to mutable vars. Ex:

let guess: u32 = "42".parse().expect("Not a number");

If we remove the :u32 from this declaration, the compiler will display an error because it cannot always infer the type, sometimes it needs more info.

Scalar types

The definition of a ScalarType is one that has a single value, as far as Rust’s concerned. There are four types:

  1. Integers
  2. Floats
  3. Booleans
  4. Characters We all know them and love them, they’re great.

Integers

Rust has a few of em. With different bit lengths, both signed and unsigned.

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

Signed and Unsigned Integers

SignedIntegers indicate that these integers can possibly be negative.

Rust also allows us to write integer literals in any of these forms:

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'
Rust’s defaults are generally good places to start: integer types default to i32. The primary situation in which you’d use isize or usize is when indexing some sort of collection.

Floats

There are two options for floats, f32 and f64. The default is the 64 bit type because of modern CPU, same speed as f32 but with more precision.

Arithmetic

All the basic arithmetic operations you expect are supported by Rust.

fn main() {
    // addition
    let sum = 5 + 10;
    // subtraction
    let difference = 95.5 - 4.3;
    // multiplication
    let product = 4 * 30;
    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1
    // remainder
    let remainder = 43 % 5;
}

Booleans

They’re true and false, the data type is bool. That’s all.

Character

Denoted by the char keyword And is the language’s most primitive alphabetic type.

fn chars_craze(){
	let c = 'z';
	let z: char = 'Z';
	let heart_eyed_cat =  😻 // type inference
	}

Chars vs Strings

Character literals with single quotes ' , as opposed to string literals, which use double quotes `“

Compound types

These group multiple values into one type, Rust has two primitive compound types:

  • Tuples
  • Arrays

Tuples

A Tuple is a general way of grouping several values with a variety of types into a single compound type., They have a fixed length once declared, and cannot expand nor shrink.

Tuples separate values using commas inside a pair of parenthesis. Example:

let my_tup: (i32, f64, u8) = (500, 5.7, 9);

These values are all bound to the my_tup variable, so we can pass them around together.

Destructuring (unpacking) Tuples:

fn main() {
    let tup = (500, 6.4, 1);
    let (x, y, z) = tup;
    println!("The value of y is: {y}");
}

The unpacking on line 3 works thanks to PatternMatching , another way to access a tuple item is using . notation and the index.

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);
 
    let five_hundred = x.0; // first element
    let six_point_four = x.1; // second element
    let one = x.2; // third
}

Arrays

Arrays contain multiple values of the same data type assigned to a single variable name. This is run of the mill stuff. Ex:

let my_array = [1,2,3,4,5];

Simple, in square brackets, comma separated. //Arrays are useful when you want your data allocated on the stack, the same as the other types we have seen so far, rather than the heap.

Arrays are fixed size as well, unlike Vectors. Which is a similar collection type provided by the standard library that is allowed to grow or shrink in size.

We can initialize an array to contain the same value for each element in it. Example:

let a = [2; 3]; // the same as [2,2,2]
let b = a[1]; //access the 2nd element in the array `a`.

// wonderful syntactic sugar Array’s are accessed using square brackets as well, as seen above.

Functions

Functions are little bundled blocks of code that we can invoke and even pass values to. Every Rust program needs a main function, to act as its entry point. Functions in Rust follow snake_case naming convention, and are declared using the fn keyword. Ex:

fn useless_fun(num: u32) -> bool {  
    if num % 77 != 0 {  
        false  
    } else {  
        true  
    }  
}

to call this function: useless_fun(2); that’s it.

Parameters

These are the special vars that are passed into the function, when passing them, we call the Arguments instead. The previous example has a parameter called num with type u32.

Function signatures

Tell us about the data types it expects for the parameters, and the type it returns. Signatures help the compiler in determining if our code’s adhering to the rules, and helps us figure out what it does. Makes writing documentations easier.

Statements and Expressions

A function’s body is a combination of the two. But what’s the difference?

  • Statements are instructions that perform some action and do NOT return a value. For example, creating a variable and assigning a value to it with the let keyword is a statement and so are function definitions themselves.
  • Expressions evaluate to a resultant value. For example, a math operation like 1+1 is an expression resulting in 2. Calling a function is an expression. Calling a macro is an expression. A new scope block created with curly brackets is an expression, for example:
fn main() {
    let y = {
        let x = 3;
        x + 1
    };
    println!("The value of y is: {y}");
}

Return values

Functions often will return a value to the bit of code that calls them. Rust doesn’t make us name return values like you do parameters, but they’re included in the signature using -> as you’ve seen in useless_fun example.

By default the return value is the value of the last expression in a function. However, if we need to return early we can then use the return keyword. This is referred to as implicitReturn and explicitReturn, respectively.

fn five() -> i32 {
    5 // implicit return of 5
}
 
fn main() {
    let x = five();
 
    println!("The value of x is: {x}");
}

Comments

Comments in Rust are denoted by // just like JavaScript and others. Multi-line comments are just multiple lines with double slashes. The idiomatic Rustacean way is to include a comment before the line it comments on, but at the end isn’t inherently wrong according to the book.

Documentation comments, like doc-strings I assume, will be discussed in chapter 14. Learning Rust - Ch.14

Control flow

The control of which code runs based on conditions met or not, booleans go brrrr.

If expressions

You’ve already seen them before, the go something like:

if condition{
	// do this
} else if some_other_cond{
	// this instead
} else{
	// this
}

The expressions of an if statement are called “arms”, just like in a match statement.

Here’s the previous example all tricked out:

fn useless_fun(num: u32) -> bool {  
    if num % 77 != 0 {  
        return false;  
    } else if num % 77 == 4 {  
        return true;  
    }    match num {  
        4 => true,  
        _ => false,  
    }}

Auto conversion to Boolean

Unlike JS or Ruby, Rust doesn’t convert non-bools to booleans!

if in a let statement

This is awesome, and I love when languages support this!

fn main() {
    let bob = true;  
	let word: &str = if bob {"yeah"} else { "no" };  
	println!("{word}");
}

Repetition and Loops

Rust has three types of loop, while, and for.

loop

This is an infinite loop that keeps executing until you the programmer explicitly tell it to stop. Ex:

fn main(){
	loop{
		println!("another one");
	}
}

This will continue to run until the user hits ctrl + c. We can add a condition to check and the break from the loop. Ex:

fn main(){
	loop{
		println!("another one");
		if some_condition {
			break
		}
	}
}

Returning values from a loop

Loops are great for retrying operations that might fail, setting a condition then to return a value. Here’s an example of how to do it:

fn main() {
    let mut counter = 0;
 
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("The result is {result}");
}

labelling loops

This is great for targeting specific loops when they’re nested.

fn main(){
	'outer: loop{
		println!("another one");
		loop{
			println!("another nested one");	
			if some_condition {
				break 'outer;
			}
		}
	}
}

While loops

While loops continue based on a predicate, on each iteration it checks before going again.

fn main(){
	let mut num: u32 = 1;
	while num <=10 {
		println!("{num}");
		number += 1;
	}
}

For loops

The best way to iterate over arrays (and other iterables I assume).

for num in (1..10).rev(){
	println!("{num}");
}

Summary

Some pretty basic stuff here, it’s always nice to see how different languages implement these basics.