--- title: "Rust Basics - Module 1: Variables, Types, Functions and Control Flow" description: "Learn Rust fundamentals - variables, scalar & compound types, functions, and control flow, with clear examples for developers who already know another language" author: "Samba Siva" published: 2026-05-04T10:45:34.716Z updated: 2026-11-03T13:14:36.738Z tags: - "Technical" - "Rust" url: "https://sambasivareddy.in/blog/rust-basics-module-1-variables-types-functions-and-control-flow" --- # Rust Basics - Module 1: Variables, Types, Functions and Control Flow Rust 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 ```rust 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. ```rust 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: ```rust 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** ```rust 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 integer - `isize` / `usize` can 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 42 ``` ### Array An array is a fixed-length sequence of the **same type** ```rust 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 100 ``` Array bounds checking is done at runtime in Rust, so an out-of-bounds access panics rather than reading garbage memory, unlike C. **Example:** ```rust 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: ```cpp #include #include int main() { std::array 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. ```rust let result = if condition { 5 } else { 10 }; // Here 5 & 10 are expressions ``` You can use `return` for early returns like: ```rust 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) ```rust if score > 90 { println!("A"); } else if score > 75 { println!("B"); } else { println!("C"); } ``` ### Loops Rust has three loop constructs: 1. `loop` - infinite loop, exit with `break` . You can also return a value from `break` 2. `while` - standard conditional loop 3. `for` - always iterates over a range or collection. ```rust // 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); } ```