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:
- Integers
- Floats
- Booleans
- 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.
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
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 literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_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.