- Published on
Chapter 3: Common Programming Concepts
15 min read
- Authors
- Name
- Mohsin Mukhtiar
- @justmohsin_

Table of Contents
- Chapter 3: Common Programming Concepts
- Variables and Mutability in Rust 🦀
- The Immutable Default
- Explicit Mutability
- Constants
- Variable Shadowing
- The Philosophy Behind Design
- ==> : Data Types
- Scalar Types: The Building Blocks
- Integers: Precision by Design
- Floating-Point: IEEE 754 Precision
- Boolean: Truth in Simplicity
- Character: Unicode by Default
- Compound Types: Grouping Data Elegantly
- Tuples: Heterogeneous Collections
- Arrays: Fixed-Size Homogeneous Collections
- Type Safety in Action
- The Power of Inference
Chapter 3: Common Programming Concepts
Variables and Mutability in Rust 🦀
When you first start learning Rust, one of the most striking differences from other programming languages is how it handles variables. Coming from languages like Python or JavaScript where you can freely change any variable's value, Rust's approach might seem restrictive. But this apparent strictness is actually one of Rust's greatest strengths, preventing entire categories of bugs before your code ever runs.
The Immutable Default
In Rust, variables are immutable by default. This means once you bind a value to a name, you cannot change that value. Let's see this in action by creating a new project:
cargo new variables
cd variables
Now, let's write some code that demonstrates this behavior:
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
When you run this code with cargo run
, Rust immediately catches the problem:
$ 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
This error isn't just the compiler being difficult—it's protecting you from bugs. Imagine a large program where one part of your code assumes a configuration value never changes, while another part modifies it. This could lead to subtle bugs that are incredibly hard to track down. Rust eliminates this possibility entirely.
Explicit Mutability
When you do need to change a variable's value, Rust requires you to be explicit about it using the mut
keyword:
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
Now the program compiles and runs successfully:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
The mut
keyword serves as a clear signal to anyone reading your code: "This variable is intended to change." This explicit declaration makes your code more readable and helps prevent accidental modifications.
Constants
While immutable variables can sometimes be changed (through shadowing, which we'll discuss), constants are truly unchangeable values. They differ from variables in several important ways:
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
fn main() {
println!("Three hours contains {} seconds", THREE_HOURS_IN_SECONDS);
}
Constants have unique characteristics:
- Always immutable: You cannot use
mut
with constants - Type annotation required: You must explicitly specify the type
- Global scope allowed: Constants can be declared anywhere, including globally
- Compile-time evaluation: They can only be set to expressions that can be computed at compile time
The naming convention for constants is ALL_UPPERCASE with underscores between words. This makes them easily identifiable and conveys their permanent nature. Constants are perfect for values like configuration limits, mathematical constants, or any value that represents a fundamental property of your system.
Variable Shadowing
Rust allows you to declare a new variable with the same name as a previous variable, a feature called shadowing. The new variable "shadows" the previous one:
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}");
}
This program produces:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
Here's what happens step by step:
- First
x
is bound to5
- A new
x
shadows the first, with value6
(5 + 1) - Inside the inner scope, another
x
shadows with value12
(6 * 2) - When the scope ends, we return to the outer
x
with value6
Shadowing differs from mutability in two important ways. First, you get compile-time protection—you must use let
to create a new binding. Second, and perhaps more importantly, shadowing allows you to change the type of a value while reusing the same name:
let spaces = " ";
let spaces = spaces.len();
The first spaces
is a string, the second is a number. This is particularly useful when transforming data from one form to another, like parsing user input from a string to a number.
If you tried to do the same thing with a mutable variable, you'd get an error:
let mut spaces = " ";
spaces = spaces.len(); // This won't compile!
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
The Philosophy Behind Design
These features—immutability by default, explicit mutability, constants, and shadowing—all serve Rust's core mission of preventing bugs while maintaining performance. By making you think carefully about when and how data changes, Rust eliminates entire categories of errors that plague other systems programming languages.
When you encounter Rust's compiler errors, remember they're not obstacles but helpful guides. Each error prevents a potential runtime bug, making your programs more reliable and secure. The habits you develop working with Rust's variable system will make you a more thoughtful programmer in any language.
As you continue your Rust journey, these fundamental concepts will support more advanced features like ownership and borrowing. The explicit thinking about data mutability you're developing now will serve as the foundation for understanding Rust's unique approach to memory safety.
==> : Data Types
Rust's type system is where the magic happens. Every value in Rust has a specific type that tells the compiler what kind of data it's working with and how much memory it occupies. This isn't just bureaucracy—it's the foundation of Rust's memory safety guarantees and zero-cost abstractions.
Rust is a statically typed language, meaning all variable types must be known at compile time. However, the compiler can often infer types based on the value and how it's used, creating a perfect balance between safety and ergonomics.
Scalar Types: The Building Blocks
Scalar types represent single values. Rust has four primary scalar types that form the backbone of all data manipulation.
Integers: Precision by Design
Rust gives you granular control over integer types, letting you choose exactly the right size for your data. Each integer type specifies both the number of bits it uses and whether it's signed or 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 |
fn main() {
let small: i8 = 127; // 8-bit signed (-128 to 127)
let big: i64 = 9_223_372_036_854_775_807; // 64-bit signed
let unsigned: u32 = 4_294_967_295; // 32-bit unsigned
// Rust infers i32 by default
let default_int = 42;
// Number literals with type suffixes
let hex = 0xff_u8; // 255 as u8
let octal = 0o77_i32; // 63 as i32
let binary = 0b1111_0000_u8; // 240 as u8
let decimal = 98_222_i32; // Underscores for readability
// Architecture-dependent sizes
let ptr_sized: isize = 100; // Same size as a pointer
let index: usize = 42; // Commonly used for array indexing
}
The _
separators make large numbers readable, and Rust's default i32
strikes the perfect balance between performance and range for most use cases. The isize
and usize
types depend on the architecture your program is running on: 64 bits on a 64-bit architecture and 32 bits on a 32-bit architecture.
Integer Overflow: In debug mode, Rust panics on integer overflow. In release mode, Rust performs two's complement wrapping. You can explicitly handle overflow with methods like wrapping_add
, checked_add
, overflowing_add
, and saturating_add
.
Floating-Point: IEEE 754 Precision
Rust provides two floating-point types following the IEEE-754 standard. The f32
type is a single-precision float, and f64
is a double-precision float:
fn main() {
let pi: f64 = 3.141592653589793; // Double precision (default)
let e: f32 = 2.718281828; // Single precision
// Mathematical operations
let sum = 5.5 + 10.2; // f64 by default
let difference = 95.5 - 4.3;
let product = 4.0 * 30.0;
let quotient = 56.7 / 32.2;
let remainder = 43.0 % 5.0;
// Floating-point methods
let negative = -42.5_f64;
let absolute = negative.abs(); // 42.5
let rounded = pi.round(); // 3.0
let ceiling = 2.1_f32.ceil(); // 3.0
let floor = 2.9_f32.floor(); // 2.0
// Special values
let infinity = f64::INFINITY;
let neg_infinity = f64::NEG_INFINITY;
let not_a_number = f64::NAN;
}
Rust defaults to f64
because modern processors make it nearly as fast as f32
while providing much better precision. All floating-point types are signed and can represent positive values, negative values, and special values like infinity and NaN (Not a Number).
Boolean: Truth in Simplicity
The boolean type is elegantly simple but crucial for control flow. Rust's bool
type has only two possible values: true
and false
, and it's exactly one byte in size:
fn main() {
let is_rust_awesome = true;
let is_learning_hard: bool = false;
// Booleans shine in conditional logic
if is_rust_awesome && !is_learning_hard {
println!("Rust is approachable and powerful!");
}
// Boolean operations
let logical_and = true && false; // false
let logical_or = true || false; // true
let logical_not = !true; // false
// Comparison operators return booleans
let greater = 5 > 3; // true
let equal = 42 == 42; // true
let not_equal = 10 != 20; // true
}
Character: Unicode by Default
Rust's char
type represents Unicode scalar values, making internationalization seamless:
fn main() {
let letter = 'z';
let emoji = '😻';
let chinese = '中';
// Each char is 4 bytes, supporting full Unicode
println!("Char size: {} bytes", std::mem::size_of::<char>());
}
Compound Types: Grouping Data Elegantly
Compound types combine multiple values into a single type, enabling sophisticated data structures.
Tuples: Heterogeneous Collections
Tuples group values of different types into a single compound type:
fn main() {
let coordinates: (i32, i32, i32) = (10, 20, 30);
let mixed_data = ("Rust", 2024, true, 3.14);
// Destructuring tuples
let (x, y, z) = coordinates;
println!("Position: x={}, y={}, z={}", x, y, z);
// Accessing by index
let language = mixed_data.0;
let year = mixed_data.1;
// The unit tuple - Rust's way of representing "nothing"
let unit: () = ();
}
The empty tuple ()
is special—it's called the unit type and represents expressions that don't return a meaningful value.
Arrays: Fixed-Size Homogeneous Collections
Arrays in Rust are fixed-size collections of the same type, allocated on the stack:
fn main() {
// Explicit type annotation
let months: [&str; 12] = [
"January", "February", "March", "April",
"May", "June", "July", "August",
"September", "October", "November", "December"
];
// Type inferred from initialization
let fibonacci = [1, 1, 2, 3, 5, 8, 13];
// Initialize with repeated values
let zeros = [0; 5]; // [0, 0, 0, 0, 0]
// Accessing elements
let first_month = months[0];
let third_fib = fibonacci[2];
println!("Array length: {}", months.len());
}
Type Safety in Action
Rust's type system prevents entire categories of bugs at compile time:
fn main() {
let number = 42;
let text = "Hello";
// This won't compile - type mismatch
// let result = number + text; // Error!
// Rust forces explicit conversion
let number_as_string = number.to_string();
let combined = number_as_string + text;
// Array bounds are checked at runtime
let arr = [1, 2, 3, 4, 5];
let index = 10;
// This will panic at runtime, but won't corrupt memory
// let element = arr[index]; // Panic: index out of bounds
}
The Power of Inference
Rust's type inference is sophisticated enough to deduce types in most situations:
fn main() {
let mut numbers = Vec::new(); // Type unknown yet
numbers.push(42); // Now Rust knows it's Vec<i32>
let collected: Vec<i32> = (0..10).collect(); // Explicit when needed
// Inference works with complex expressions
let result = numbers.iter().map(|x| x * 2).collect::<Vec<i32>>();
}
Rust's type system isn't just about preventing bugs—it's about expressing intent clearly and enabling the compiler to generate optimal code. By understanding these fundamental types, you're building the mental model needed to leverage Rust's unique approach to systems programming, where safety and performance aren't mutually exclusive choices.
The beauty lies in how these simple building blocks combine to create complex, safe, and efficient programs. Every type decision you make is a step toward more reliable software.