Rust Basics - Module 1: Variables, Types, Functions and Control Flow
Part 1 of 5 in Rust BasicsRust is a general-purpose programming language which emphasizes performance, type safety, concurrency and memory safety.
Rust can be used to build the CLI, WebAssembly, Networking, Embedded systems and enforces the performance, reliability and productivity.
In this series, we'll explore Rust from the ground up - starting with variables, types, functions, and control flow.
Part 1: Variable
Variable is a named memory location where the data is stored. By default, the variables are immutable, that is once assigned cannot be changed.
let
fn main() {
let num = 5;
num = 6; // Compile Error: Cannot assign twice to immutable variable
}To make a variable mutable, one must specify explicitly using mut keyword.
fn main() {
let mut num = 5;
num = 6; // Allowed now
}const
There is a const, which is differs from let in a few important ways. A const must have its type annotated, and its value must be computable at compile time and inlined whatever it defined.
A const lives for the entire duration of the program.
const MAX_CONNECTIONS: u32 = 100;Here, MAX_CONNECTIONS is type-annotated as u32, and 100 is a value the compiler can resolve at compile time - not at runtime.
Shadowing
There is also "shadowing" i.e you can re-declare a variable with the same name using let again in the same scope:
fn main() {
let num = 5;
let num = num + 1; // Shadows the previous num
let num = num * 2; // Shadows again --> num is now 12
}You might wonder: can't we do the same with mut? The key difference is that each let can also change the type, which mut doesn't allow.
Example
fn main() {
let x = 5; // Numeric u32
let x = String::from("5"); // String
}Immutability by default means the compiler can make stronger guarantees about who can modify what.
Part 2: Scalar Types
Rust is statically typed language. The compiler infers types most of the time without need of defining explicitly, but you can (and sometimes must) annotate explicitly like in cases of const .
The four scalar categories:
Integers
i8/u8, i16/u16, i32/u32 (default), i64/u64, i128/u128, isize/usize
8,16,32,64,128 represent the bits.
i= signed integer,u= unsigned integerisize/usizecan be 4 or 8 bytes depending on the target architecture.
Floating Point
f32 and f64
Default: f64
Boolean: true/false
Character: char, always 4 bytes, represents a Unicode scalar
Integer Overflow
In debug mode (will discuss more later), Rust panics (throws an error) on overflow. In release mode it wraps around (e.g., for u8 , 255 + 1 becomes 0).
Beyond scalar types, Rust also provides two built-in compound types for grouping values: tuples and arrays.
Part 3: Compound Types - Tuples & Arrays
Compound types allows to store the collection of values under one variable. There are the two fixed-size compound types in Rust i.e Tuples and Arrays.
Tuple
A tuple groups values of different types
let tup: (i32, f64, bool) = (42, 3.14, true);
let (x, y, z) = tup; // destructure i.e x = 42, y = 3.14, z = true
let first = tup.0; // index access returns 42Array
An array is a fixed-length sequence of the same type
let arr: [i32; 5] = [1, 2, 3, 4, 5]; // i32 array of size 5
let zeroes = [0; 100]; // a i32 array of 100 zeroes (0) i.e size is 100Array bounds checking is done at runtime in Rust, so an out-of-bounds access panics rather than reading garbage memory, unlike C.
Example:
fn main() {
let arr: [i32; 5] = [1, 2, 3, 4, 5];
println!(arr[5]); // Panics at compile time
}In C/C++, the above code might print garbage values and you'd need to handle bounds checking explicitly, like below:
#include <iostream>
#include <array>
int main() {
std::array<int, 5> arr = {1, 2, 3, 4, 5};
try {
std::cout << arr.at(5) << std::endl; // .at() checks bounds
} catch (const std::out_of_range& e) {
std::cerr << "Caught error: " << e.what() << std::endl;
}
return 0;
}Part 4: Functions
Functions in Rust use the keyword fn . The return type is declared with -> . A function body is a block {} and the last expression without semicolon ; is the return value.
fn function_name(param1: type, param2: type, ...) -> return_type {
// ...code
}Example
fn add(a: i32, b: i32) -> i32 {
a + b // no semi-colon = this is the return value
}This is the expression-based nature of Rust. An expression produces a value. A statement terminated by ; does not. This distinction matters everywhere in Rust: if, match, and blocks {} can all be used as expressions.
let result = if condition { 5 } else { 10 }; // Here 5 & 10 are expressionsYou can use return for early returns like:
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
return 0.0; // early return
}
a / b // expression return
}Function parameters always require type annotations, there is no type inference on function signatures. This is intentional in Rust, function signatures serve as the API contract (more on this in upcoming modules).
Part 5: Control Flow
Conditional
if / else if / else works as expected, with one exception i.e., the condition must be exactly boolean. Rust has no truthiness coercion (if 1 is a compile error)
if score > 90 {
println!("A");
} else if score > 75 {
println!("B");
} else {
println!("C");
}Loops
Rust has three loop constructs:
loop- infinite loop, exit withbreak. You can also return a value frombreakwhile- standard conditional loopfor- always iterates over a range or collection.
// loop
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 { break counter * 2; }
} // result = 20
// while
let mut n = 2;
while n < 100 {
n *= 2;
} // n = 128
// for
for i in 0..5 { // 0 inclusive, 5 exclusive
println!("{}", i); // 0,1,2,3,4
}
for i in 0..=5 { // 0 & 5 both inclusive
println!("{}", i); // 0,1,2,3,4,5
}
let arr: [i32; 5] = [0,1,2,3,4];
for element in arr.iter() {
println!("{}", element); // 0,1,2,3,4
}Complete Example
Here's a complete program that ties together everything covered in Parts 1–5 — constants, arrays, functions, and control flow - to compute and grade a list of scores.
const MAX_SCORE: u32 = 100;
fn clamp(value: u32, min: u32, max: u32) -> u32 {
if value < min {
return min;
}
if value > max {
return max;
}
value // expression return — no semicolon
}
fn grade(score: u32) -> &'static str {
if score >= 90 { "A" }
else if score >= 75 { "B" }
else if score >= 60 { "C" }
else { "F" }
}
fn main() {
let scores: [u32; 5] = [72, 95, 110, 45, 83];
for raw in scores.iter() {
let clamped = clamp(*raw, 0, MAX_SCORE);
let letter = grade(clamped);
println!("Score: {:>3} → {}", clamped, letter);
}
let mut total: u32 = 0;
let mut count: u32 = 0;
for &s in scores.iter() {
total += clamp(s, 0, MAX_SCORE);
count += 1;
}
let avg = total / count;
println!("Average: {}", avg);
}Enjoyed this post?
2 reactions