🦀 Introduction to the Rust Programming Language

Andrew J. Pounds, Ph.D.

🎯 Learning Objectives

After completing this exercise, you will be able to:

1 What is Rust?

Rust is a systems programming language developed by Mozilla Research, with its first stable release in 2015. It was designed to be a safe, concurrent, and practical language that gives programmers control over low-level details while preventing common programming errors at compile time.

Rust consistently ranks as one of the most loved programming languages in developer surveys due to its unique combination of safety and performance. Key features include:

Rust source files use the .rs extension. We will use both the Rust compiler (rustc) for simple programs and Cargo for project management.

2 Your First Rust Program

Create a file called hello.rs with the following content:

fn main() {
    println!("Hello, World!");
}

Compiling with rustc

Open a terminal and navigate to the directory containing your file. Compile and run with:

rustc hello.rs
./hello

You should see Hello, World! printed to the console.

Using Cargo

For larger projects, use Cargo, Rust's build system and package manager:

# Create a new project
cargo new hello_cargo
cd hello_cargo

# Build and run
cargo run

Cargo creates a project structure with src/main.rs containing a hello world program.

💡 Tip: Use rustc for quick single-file experiments. Use cargo for any real project—it handles dependencies, building, testing, and more.

Understanding the Code

Code Element Explanation
fn main() Declares the main function—the entry point of every Rust program
{ } Curly braces define the function body (block)
println!(...) A macro that prints text followed by a newline (note the !)
; Semicolon ends the statement
📝 Note: Rust is case-sensitive. Main, main, and MAIN are all different identifiers. By convention, function names use snake_case (lowercase with underscores).

Cargo Project Structure

When you run cargo new project_name, Cargo creates:

project_name/
├── Cargo.toml      # Project configuration and dependencies
└── src/
    └── main.rs     # Your main source file

The Cargo.toml file contains project metadata:

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

[dependencies]

3 Variable Types

Rust is a statically typed language with type inference. You must declare variables with let, but the compiler can often infer the type automatically.

Integer Types

Type Size Range
i8 8 bits -128 to 127
i16 16 bits -32,768 to 32,767
i32 32 bits -2,147,483,648 to 2,147,483,647
i64 64 bits -9.2Ă—1018 to 9.2Ă—1018
i128 128 bits Very large range
isize Platform dependent 32 or 64 bits based on architecture

Unsigned types are also available: u8, u16, u32, u64, u128, usize.

📝 Note: The default integer type is i32. When you write let x = 42; without a type annotation, Rust infers i32.

Floating-Point Types

Type Size Description
f32 32 bits Single precision (~7 significant digits)
f64 64 bits Double precision (~15 significant digits)
📝 Note: The default floating-point type is f64. When you write let x = 3.14;, Rust infers f64.

Other Fundamental Types

Type Description Example
bool Boolean (true or false) let flag: bool = true;
char Unicode character (4 bytes) let letter: char = 'A';
String Growable, owned string let name = String::from("Alice");
&str String slice (borrowed) let greeting: &str = "Hello";

Variable Declaration Examples

fn main() {
    // Type annotation (explicit)
    let age: i32 = 25;
    let gpa: f64 = 3.85;
    let name: &str = "Alice";
    let enrolled: bool = true;
    let grade: char = 'A';
    
    // Type inference (compiler figures out the type)
    let count = 100;        // inferred as i32
    let price = 19.99;      // inferred as f64
    let message = "Hello";  // inferred as &str
    
    // Output the values
    println!("Name: {}", name);
    println!("Age: {}", age);
    println!("GPA: {:.2}", gpa);
    println!("Enrolled: {}", enrolled);
    println!("Grade: {}", grade);
}
Name: Alice Age: 25 GPA: 3.85 Enrolled: true Grade: A

Mutability

By default, variables in Rust are immutable. To make a variable mutable, use mut:

fn main() {
    let x = 5;          // immutable
    // x = 6;           // ERROR: cannot assign twice to immutable variable
    
    let mut y = 5;      // mutable
    println!("y = {}", y);
    y = 6;              // OK: y is mutable
    println!("y = {}", y);
}
y = 5 y = 6
⚠️ Important: Immutability by default is a key Rust feature that helps prevent bugs. Only use mut when you actually need to change a variable's value.

Constants

Use const for values that never change and must be known at compile time:

const MAX_POINTS: u32 = 100_000;
const PI: f64 = 3.14159265359;

fn main() {
    println!("Max points: {}", MAX_POINTS);
    println!("Pi: {}", PI);
}
📝 Note: Constants must have explicit type annotations and use SCREAMING_SNAKE_CASE by convention. You can use underscores in numeric literals for readability: 100_000 is the same as 100000.

Shadowing

Rust allows you to declare a new variable with the same name, which "shadows" the previous one:

fn main() {
    let x = 5;
    println!("x = {}", x);
    
    let x = x + 1;  // shadows previous x
    println!("x = {}", x);
    
    let x = x * 2;  // shadows again
    println!("x = {}", x);
    
    // Can even change type when shadowing
    let spaces = "   ";         // &str
    let spaces = spaces.len();  // usize (shadows with different type)
    println!("spaces = {}", spaces);
}
x = 5 x = 6 x = 12 spaces = 3

4 Console Output

Rust provides several macros for output. The ! indicates these are macros, not regular functions.

The println! and print! Macros

Macro Description
println!(...) Prints text followed by a newline
print!(...) Prints text without a newline
eprintln!(...) Prints to stderr with newline (for errors)
eprint!(...) Prints to stderr without newline
fn main() {
    // Basic output
    println!("Hello, World!");
    
    // Output without newline
    print!("This ");
    print!("is ");
    print!("on one line");
    println!();  // Just prints a newline
    
    // Output to stderr (for error messages)
    eprintln!("This is an error message");
}
Hello, World! This is on one line This is an error message

Formatting with Placeholders

Use {} as placeholders for values:

fn main() {
    let name = "Alice";
    let age = 25;
    let gpa = 3.85;
    
    // Basic placeholders
    println!("Name: {}", name);
    println!("Age: {}", age);
    println!("GPA: {}", gpa);
    
    // Multiple values
    println!("{} is {} years old", name, age);
    
    // Positional arguments
    println!("{0} is {1}. {0} studies hard.", name, age);
    
    // Named arguments
    println!("{student} has a {score} GPA", student = name, score = gpa);
}
Name: Alice Age: 25 GPA: 3.85 Alice is 25 years old Alice is 25. Alice studies hard. Alice has a 3.85 GPA

Formatting Numbers

fn main() {
    let pi = 3.14159265359;
    let big_number = 1234567;
    let percentage = 0.856;
    
    // Decimal places
    println!("Pi: {:.2}", pi);           // 2 decimal places
    println!("Pi: {:.4}", pi);           // 4 decimal places
    
    // Width and alignment
    println!("[{:10}]", 42);             // right-aligned, width 10
    println!("[{:<10}]", 42);            // left-aligned, width 10
    println!("[{:^10}]", 42);            // center-aligned, width 10
    println!("[{:0>10}]", 42);           // zero-padded, width 10
    
    // Combined width and precision
    println!("Value: {:8.2}", pi);       // width 8, 2 decimal places
    
    // Different bases
    println!("Decimal: {}", 255);
    println!("Binary: {:b}", 255);
    println!("Octal: {:o}", 255);
    println!("Hex (lower): {:x}", 255);
    println!("Hex (upper): {:X}", 255);
}
Pi: 3.14 Pi: 3.1416 [ 42] [42 ] [ 42 ] [0000000042] Value: 3.14 Decimal: 255 Binary: 11111111 Octal: 377 Hex (lower): ff Hex (upper): FF

Formatting Strings

fn main() {
    let name = "Alice";
    
    // Width for strings
    println!("[{:15}]", name);           // right-padded to 15 chars
    println!("[{:>15}]", name);          // right-aligned
    println!("[{:^15}]", name);          // centered
    
    // Truncation
    println!("[{:.3}]", "Hello");        // truncate to 3 chars
}
[Alice ] [ Alice] [ Alice ] [Hel]
? Tip: For debugging, use {:?} (debug format) or {:#?} (pretty debug format) to print complex data structures.
fn main() {
    let numbers = [1, 2, 3, 4, 5];
    
    println!("Debug: {:?}", numbers);
    println!("Pretty debug: {:#?}", numbers);
}
Debug: [1, 2, 3, 4, 5] Pretty debug: [ 1, 2, 3, 4, 5, ]

5 Console Input

Reading input in Rust requires the std::io module. Input is always read as a String and must be parsed to other types.

Basic Input Reading

use std::io;

fn main() {
    println!("What is your name?");
    
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read line");
    
    // trim() removes the trailing newline
    let name = input.trim();
    
    println!("Hello, {}!", name);
}
What is your name? Alice Hello, Alice!

Reading Numbers

To read numbers, parse the string input:

use std::io;

fn main() {
    println!("Enter your age:");
    
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read line");
    
    let age: i32 = input.trim().parse().expect("Please enter a valid number");
    
    println!("You are {} years old.", age);
    println!("Next year you will be {}.", age + 1);
}
Enter your age: 25 You are 25 years old. Next year you will be 26.

Handling Parse Errors

Use match to handle invalid input gracefully:

use std::io;

fn main() {
    println!("Enter a number:");
    
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read line");
    
    let number: i32 = match input.trim().parse() {
        Ok(n) => n,
        Err(_) => {
            println!("That's not a valid number!");
            return;
        }
    };
    
    println!("You entered: {}", number);
}

Creating a Helper Function for Input

Since input reading is verbose, create a reusable function:

use std::io::{self, Write};

fn read_line(prompt: &str) -> String {
    print!("{}", prompt);
    io::stdout().flush().unwrap();  // Ensure prompt is displayed
    
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read line");
    
    input.trim().to_string()
}

fn read_i32(prompt: &str) -> i32 {
    loop {
        let input = read_line(prompt);
        match input.parse() {
            Ok(n) => return n,
            Err(_) => println!("Invalid number. Please try again."),
        }
    }
}

fn read_f64(prompt: &str) -> f64 {
    loop {
        let input = read_line(prompt);
        match input.parse() {
            Ok(n) => return n,
            Err(_) => println!("Invalid number. Please try again."),
        }
    }
}

fn main() {
    let name = read_line("Enter your name: ");
    let age = read_i32("Enter your age: ");
    let gpa = read_f64("Enter your GPA: ");
    
    println!();
    println!("Name: {}", name);
    println!("Age: {}", age);
    println!("GPA: {:.2}", gpa);
}
Enter your name: Alice Enter your age: 25 Enter your GPA: 3.85 Name: Alice Age: 25 GPA: 3.85
? Note: The io::stdout().flush().unwrap() call ensures that print! output appears immediately before waiting for input. Without it, the prompt might not display until after you press Enter.

6 Assignment Statements and Operators

Assignment

In Rust, = is for assignment and == is for equality comparison:

fn main() {
    let mut x = 10;     // initial assignment
    println!("x = {}", x);
    
    x = 20;             // reassignment (requires mut)
    println!("x = {}", x);
    
    x = x + 5;          // compound expression
    println!("x = {}", x);
}
x = 10 x = 20 x = 25

Arithmetic Operators

Operator Operation Example Result
+ Addition 5 + 3 8
- Subtraction 5 - 3 2
* Multiplication 5 * 3 15
/ Division 7 / 3 2 (integer division)
% Modulus (remainder) 7 % 3 1
? Note: Integer division truncates toward zero. For floating-point division, at least one operand must be a float: 7.0 / 3.0 gives 2.333...

Compound Assignment Operators

fn main() {
    let mut x = 10;
    
    x += 5;    // x = x + 5
    println!("After += 5: {}", x);
    
    x -= 3;    // x = x - 3
    println!("After -= 3: {}", x);
    
    x *= 2;    // x = x * 2
    println!("After *= 2: {}", x);
    
    x /= 4;    // x = x / 4
    println!("After /= 4: {}", x);
    
    x %= 3;    // x = x % 3
    println!("After %= 3: {}", x);
}
After += 5: 15 After -= 3: 12 After *= 2: 24 After /= 4: 6 After %= 3: 0
⚠️ Important: Rust does NOT have ++ or -- operators. Use x += 1 or x -= 1 instead.

Comparison Operators

Operator Meaning Example
== Equal to x == 5
!= Not equal to x != 5
< Less than x < 10
> Greater than x > 10
<= Less than or equal x <= 10
>= Greater than or equal x >= 10

Logical Operators

Operator Meaning Example
&& Logical AND x > 0 && x < 100
|| Logical OR x < 0 || x > 100
! Logical NOT !flag
fn main() {
    let age = 25;
    let has_license = true;
    
    // Comparison operators
    println!("age == 25: {}", age == 25);
    println!("age != 30: {}", age != 30);
    println!("age > 18: {}", age > 18);
    
    // Logical operators
    println!("Can drive: {}", age >= 16 && has_license);
    println!("Is teenager: {}", age >= 13 && age <= 19);
    println!("NOT has_license: {}", !has_license);
}
age == 25: true age != 30: true age > 18: true Can drive: true Is teenager: false NOT has_license: false

Type Casting

Use as to convert between numeric types:

fn main() {
    let x: i32 = 10;
    let y: f64 = 3.0;
    
    // Convert i32 to f64 for floating-point division
    let result = x as f64 / y;
    println!("10 / 3.0 = {:.4}", result);
    
    // Convert f64 to i32 (truncates)
    let pi = 3.14159;
    let truncated = pi as i32;
    println!("{} as i32 = {}", pi, truncated);
    
    // Convert between integer sizes
    let small: i8 = 42;
    let big: i64 = small as i64;
    println!("i8 {} as i64 = {}", small, big);
}
10 / 3.0 = 3.3333 3.14159 as i32 = 3 i8 42 as i64 = 42
? Note: Rust requires explicit type conversions—it won't automatically convert between types. This prevents subtle bugs that can occur with implicit conversions.

7 Control Structures

Rust provides several constructs for controlling program flow based on conditions.

The if Expression

The basic if statement tests a condition and executes code accordingly:

fn main() {
    let number = 7;
    
    if number > 5 {
        println!("{} is greater than 5", number);
    }
}
7 is greater than 5
⚠️ Important: Unlike C/C++ or Java, Rust conditions do NOT require parentheses, but the curly braces are always required—even for single statements.

if-else

fn main() {
    let number = 3;
    
    if number > 5 {
        println!("{} is greater than 5", number);
    } else {
        println!("{} is not greater than 5", number);
    }
}
3 is not greater than 5

if-else if-else

fn main() {
    let score = 85;
    
    if score >= 90 {
        println!("Grade: A");
    } else if score >= 80 {
        println!("Grade: B");
    } else if score >= 70 {
        println!("Grade: C");
    } else if score >= 60 {
        println!("Grade: D");
    } else {
        println!("Grade: F");
    }
}
Grade: B

if as an Expression

In Rust, if is an expression that returns a value:

fn main() {
    let condition = true;
    
    // if can return a value
    let number = if condition { 5 } else { 6 };
    
    println!("The number is: {}", number);
    
    // More complex example
    let score = 85;
    let grade = if score >= 90 {
        'A'
    } else if score >= 80 {
        'B'
    } else if score >= 70 {
        'C'
    } else if score >= 60 {
        'D'
    } else {
        'F'
    };
    
    println!("Grade: {}", grade);
}
The number is: 5 Grade: B
? Note: When using if as an expression, both branches must return the same type. Notice there's no semicolon after the value in each branch—this makes it a return value rather than a statement.

The match Expression

The match expression is a powerful control flow construct for pattern matching:

fn main() {
    let number = 3;
    
    match number {
        1 => println!("One"),
        2 => println!("Two"),
        3 => println!("Three"),
        4 => println!("Four"),
        5 => println!("Five"),
        _ => println!("Something else"),  // _ is the catch-all pattern
    }
}
Three
⚠️ Important: The match expression must be exhaustive—it must cover every possible value. The _ pattern matches anything not explicitly listed.

match with Multiple Patterns and Ranges

fn main() {
    let score = 85;
    
    let grade = match score {
        90..=100 => 'A',      // Range from 90 to 100 (inclusive)
        80..=89 => 'B',
        70..=79 => 'C',
        60..=69 => 'D',
        0..=59 => 'F',
        _ => '?',             // Invalid score
    };
    
    println!("Score: {}, Grade: {}", score, grade);
    
    // Multiple patterns with |
    let day = 6;
    let day_type = match day {
        1 | 2 | 3 | 4 | 5 => "Weekday",
        6 | 7 => "Weekend",
        _ => "Invalid day",
    };
    
    println!("Day {} is a {}", day, day_type);
}
Score: 85, Grade: B Day 6 is a Weekend

match with Strings

fn main() {
    let command = "quit";
    
    match command {
        "start" => println!("Starting..."),
        "stop" => println!("Stopping..."),
        "pause" => println!("Pausing..."),
        "quit" | "exit" => println!("Goodbye!"),
        _ => println!("Unknown command: {}", command),
    }
}
Goodbye!

match with Guards

You can add conditions to match arms with if guards:

fn main() {
    let number = 4;
    
    match number {
        n if n < 0 => println!("{} is negative", n),
        n if n == 0 => println!("zero"),
        n if n % 2 == 0 => println!("{} is positive and even", n),
        n => println!("{} is positive and odd", n),
    }
}
4 is positive and even

Nested Conditions

fn main() {
    let age = 25;
    let has_id = true;
    
    if age >= 18 {
        if has_id {
            println!("Entry permitted.");
        } else {
            println!("ID required for entry.");
        }
    } else {
        println!("Must be 18 or older.");
    }
}
Entry permitted.

8 Loops

Rust provides three types of loops: loop, while, and for.

The loop Statement (Infinite Loop)

The loop keyword creates an infinite loop. Use break to exit:

fn main() {
    let mut counter = 0;
    
    loop {
        counter += 1;
        println!("Counter: {}", counter);
        
        if counter >= 5 {
            break;
        }
    }
    
    println!("Loop finished!");
}
Counter: 1 Counter: 2 Counter: 3 Counter: 4 Counter: 5 Loop finished!

Returning Values from loop

The loop expression can return a value with break:

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

The while Loop

The while loop continues while a condition is true:

fn main() {
    let mut number = 5;
    
    while number > 0 {
        println!("{}!", number);
        number -= 1;
    }
    
    println!("Liftoff!");
}
5! 4! 3! 2! 1! Liftoff!

The for Loop

The for loop iterates over collections or ranges:

fn main() {
    // Iterating over a range (1 to 5, exclusive of 6)
    println!("Counting 1 to 5:");
    for i in 1..6 {
        println!("  {}", i);
    }
    
    // Inclusive range (1 to 5, inclusive)
    println!("\nUsing inclusive range:");
    for i in 1..=5 {
        println!("  {}", i);
    }
}
Counting 1 to 5: 1 2 3 4 5 Using inclusive range: 1 2 3 4 5
? Note: Use a..b for a range from a to b-1 (exclusive end), or a..=b for a range from a to b (inclusive end).

Iterating Over Arrays

fn main() {
    let numbers = [10, 20, 30, 40, 50];
    
    // Iterate over elements
    println!("Elements:");
    for num in numbers {
        println!("  {}", num);
    }
    
    // Iterate with index using enumerate()
    println!("\nWith indices:");
    for (index, num) in numbers.iter().enumerate() {
        println!("  numbers[{}] = {}", index, num);
    }
}
Elements: 10 20 30 40 50 With indices: numbers[0] = 10 numbers[1] = 20 numbers[2] = 30 numbers[3] = 40 numbers[4] = 50

Reverse Iteration

fn main() {
    println!("Countdown:");
    for i in (1..=5).rev() {
        println!("  {}", i);
    }
    println!("  Liftoff!");
}
Countdown: 5 4 3 2 1 Liftoff!

Loop Control: break and continue

fn main() {
    // break - exit the loop early
    println!("Using break:");
    for i in 1..10 {
        if i == 5 {
            println!("  Breaking at {}", i);
            break;
        }
        println!("  {}", i);
    }
    
    // continue - skip to next iteration
    println!("\nUsing continue (skip even numbers):");
    for i in 1..=10 {
        if i % 2 == 0 {
            continue;  // Skip even numbers
        }
        println!("  {}", i);
    }
}
Using break: 1 2 3 4 Breaking at 5 Using continue (skip even numbers): 1 3 5 7 9

Labeled Loops

Use labels to break or continue outer loops from nested loops:

fn main() {
    'outer: for i in 1..=3 {
        println!("Outer loop: i = {}", i);
        
        for j in 1..=3 {
            println!("  Inner loop: j = {}", j);
            
            if i == 2 && j == 2 {
                println!("  Breaking outer loop!");
                break 'outer;
            }
        }
    }
    
    println!("Done!");
}
Outer loop: i = 1 Inner loop: j = 1 Inner loop: j = 2 Inner loop: j = 3 Outer loop: i = 2 Inner loop: j = 1 Inner loop: j = 2 Breaking outer loop! Done!

Common Loop Patterns

fn main() {
    // Sum numbers 1 to 10
    let mut sum = 0;
    for i in 1..=10 {
        sum += i;
    }
    println!("Sum of 1-10: {}", sum);
    
    // Find factorial
    let n = 5;
    let mut factorial = 1;
    for i in 1..=n {
        factorial *= i;
    }
    println!("{}! = {}", n, factorial);
    
    // Count iterations
    let mut count = 0;
    for _ in 0..100 {  // _ ignores the loop variable
        count += 1;
    }
    println!("Counted to: {}", count);
}
Sum of 1-10: 55 5! = 120 Counted to: 100

Input Validation Loop

use std::io::{self, Write};

fn main() {
    let number: i32;
    
    loop {
        print!("Enter a positive number: ");
        io::stdout().flush().unwrap();
        
        let mut input = String::new();
        io::stdin().read_line(&mut input).expect("Failed to read");
        
        match input.trim().parse() {
            Ok(n) if n > 0 => {
                number = n;
                break;
            }
            Ok(_) => println!("Please enter a POSITIVE number."),
            Err(_) => println!("Invalid input. Please enter a number."),
        }
    }
    
    println!("You entered: {}", number);
}
Enter a positive number: -5 Please enter a POSITIVE number. Enter a positive number: abc Invalid input. Please enter a number. Enter a positive number: 42 You entered: 42

9 Functions

Functions in Rust are declared with the fn keyword. They allow you to organize code into reusable blocks.

Basic Function Syntax

fn greet() {
    println!("Hello, World!");
}

fn main() {
    greet();  // Call the function
    greet();  // Call it again
}
Hello, World! Hello, World!

Functions with Parameters

fn greet_person(name: &str) {
    println!("Hello, {}!", name);
}

fn print_sum(a: i32, b: i32) {
    println!("{} + {} = {}", a, b, a + b);
}

fn main() {
    greet_person("Alice");
    greet_person("Bob");
    
    print_sum(5, 3);
    print_sum(10, 20);
}
Hello, Alice! Hello, Bob! 5 + 3 = 8 10 + 20 = 30
⚠️ Important: You must declare the type of each parameter. Rust does not infer parameter types.

Functions with Return Values

Use -> to specify the return type. The last expression without a semicolon is returned:

fn add(a: i32, b: i32) -> i32 {
    a + b  // No semicolon = return value
}

fn multiply(a: i32, b: i32) -> i32 {
    return a * b;  // Explicit return also works
}

fn is_even(n: i32) -> bool {
    n % 2 == 0
}

fn main() {
    let sum = add(5, 3);
    println!("5 + 3 = {}", sum);
    
    let product = multiply(4, 7);
    println!("4 * 7 = {}", product);
    
    println!("Is 10 even? {}", is_even(10));
    println!("Is 7 even? {}", is_even(7));
}
5 + 3 = 8 4 * 7 = 28 Is 10 even? true Is 7 even? false
? Note: The difference between a + b (expression, returns value) and a + b; (statement, returns nothing) is crucial. Forgetting to omit the semicolon is a common mistake.

Functions with Multiple Returns (Tuples)

fn min_max(a: i32, b: i32) -> (i32, i32) {
    if a < b {
        (a, b)
    } else {
        (b, a)
    }
}

fn divide_with_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
    let quotient = dividend / divisor;
    let remainder = dividend % divisor;
    (quotient, remainder)
}

fn main() {
    let (min, max) = min_max(10, 5);
    println!("Min: {}, Max: {}", min, max);
    
    let (quot, rem) = divide_with_remainder(17, 5);
    println!("17 / 5 = {} remainder {}", quot, rem);
}
Min: 5, Max: 10 17 / 5 = 3 remainder 2

Function Examples

// Calculate factorial
fn factorial(n: u64) -> u64 {
    if n <= 1 {
        1
    } else {
        n * factorial(n - 1)
    }
}

// Check if a number is prime
fn is_prime(n: u32) -> bool {
    if n < 2 {
        return false;
    }
    if n == 2 {
        return true;
    }
    if n % 2 == 0 {
        return false;
    }
    
    let mut i = 3;
    while i * i <= n {
        if n % i == 0 {
            return false;
        }
        i += 2;
    }
    true
}

// Calculate average of three numbers
fn average(a: f64, b: f64, c: f64) -> f64 {
    (a + b + c) / 3.0
}

fn main() {
    println!("5! = {}", factorial(5));
    println!("10! = {}", factorial(10));
    
    println!("Is 17 prime? {}", is_prime(17));
    println!("Is 18 prime? {}", is_prime(18));
    
    println!("Average of 85, 90, 78: {:.2}", average(85.0, 90.0, 78.0));
}
5! = 120 10! = 3628800 Is 17 prime? true Is 18 prime? false Average of 85, 90, 78: 84.33

10 Arrays and Vectors

Rust has two main sequential collection types: fixed-size arrays and growable vectors.

Arrays (Fixed Size)

Arrays have a fixed size known at compile time:

fn main() {
    // Array with explicit type annotation
    let numbers: [i32; 5] = [1, 2, 3, 4, 5];
    
    // Array with inferred type
    let grades = [85, 90, 78, 92, 88];
    
    // Array initialized with same value
    let zeros = [0; 10];  // 10 elements, all 0
    
    // Accessing elements
    println!("First number: {}", numbers[0]);
    println!("Third grade: {}", grades[2]);
    
    // Array length
    println!("Array length: {}", numbers.len());
    
    // Print all elements
    println!("All grades: {:?}", grades);
}
First number: 1 Third grade: 78 Array length: 5 All grades: [85, 90, 78, 92, 88]
? Note: Array indices start at 0 in Rust. Accessing an invalid index causes a panic (runtime error) rather than undefined behavior.

Iterating Over Arrays

fn main() {
    let numbers = [10, 20, 30, 40, 50];
    
    // Iterate over elements
    println!("Elements:");
    for num in numbers {
        println!("  {}", num);
    }
    
    // Iterate with index
    println!("\nWith indices:");
    for (i, num) in numbers.iter().enumerate() {
        println!("  numbers[{}] = {}", i, num);
    }
    
    // Calculate sum
    let mut sum = 0;
    for num in numbers {
        sum += num;
    }
    println!("\nSum: {}", sum);
}
Elements: 10 20 30 40 50 With indices: numbers[0] = 10 numbers[1] = 20 numbers[2] = 30 numbers[3] = 40 numbers[4] = 50 Sum: 150

Vectors (Dynamic Size)

Vectors can grow or shrink at runtime:

fn main() {
    // Create an empty vector
    let mut numbers: Vec<i32> = Vec::new();
    
    // Add elements
    numbers.push(10);
    numbers.push(20);
    numbers.push(30);
    
    println!("After pushing: {:?}", numbers);
    println!("Length: {}", numbers.len());
    
    // Create with initial values using vec! macro
    let mut grades = vec![85, 90, 78];
    grades.push(92);
    grades.push(88);
    
    println!("Grades: {:?}", grades);
    
    // Access elements
    println!("First grade: {}", grades[0]);
    
    // Safe access with get() - returns Option
    match grades.get(10) {
        Some(grade) => println!("Grade at 10: {}", grade),
        None => println!("No grade at index 10"),
    }
}
After pushing: [10, 20, 30] Length: 3 Grades: [85, 90, 78, 92, 88] First grade: 85 No grade at index 10

Common Vector Operations

fn main() {
    let mut nums = vec![3, 1, 4, 1, 5, 9, 2, 6];
    
    println!("Original: {:?}", nums);
    println!("Length: {}", nums.len());
    println!("Is empty: {}", nums.is_empty());
    
    // First and last elements
    println!("First: {:?}", nums.first());
    println!("Last: {:?}", nums.last());
    
    // Remove last element
    let popped = nums.pop();
    println!("Popped: {:?}", popped);
    println!("After pop: {:?}", nums);
    
    // Insert at position
    nums.insert(0, 100);
    println!("After insert at 0: {:?}", nums);
    
    // Remove at position
    nums.remove(0);
    println!("After remove at 0: {:?}", nums);
    
    // Check if contains value
    println!("Contains 4: {}", nums.contains(&4));
    
    // Clear all elements
    nums.clear();
    println!("After clear: {:?}", nums);
    println!("Is empty: {}", nums.is_empty());
}
Original: [3, 1, 4, 1, 5, 9, 2, 6] Length: 8 Is empty: false First: Some(3) Last: Some(6) Popped: Some(6) After pop: [3, 1, 4, 1, 5, 9, 2] After insert at 0: [100, 3, 1, 4, 1, 5, 9, 2] After remove at 0: [3, 1, 4, 1, 5, 9, 2] Contains 4: true After clear: [] Is empty: true

Vector with Strings

fn main() {
    let mut names: Vec<String> = Vec::new();
    
    names.push(String::from("Alice"));
    names.push(String::from("Bob"));
    names.push(String::from("Charlie"));
    
    println!("Names: {:?}", names);
    
    // Iterate and print
    for name in &names {
        println!("Hello, {}!", name);
    }
    
    // Using vec! macro with strings
    let fruits = vec!["apple", "banana", "cherry"];
    for fruit in &fruits {
        println!("I like {}.", fruit);
    }
}
Names: ["Alice", "Bob", "Charlie"] Hello, Alice! Hello, Bob! Hello, Charlie! I like apple. I like banana. I like cherry.

Calculating Statistics

fn main() {
    let scores = vec![85.0, 92.0, 78.0, 90.0, 88.0];
    
    // Sum using iterator
    let sum: f64 = scores.iter().sum();
    println!("Sum: {}", sum);
    
    // Average
    let average = sum / scores.len() as f64;
    println!("Average: {:.2}", average);
    
    // Find min and max
    let min = scores.iter().cloned().reduce(f64::min).unwrap();
    let max = scores.iter().cloned().reduce(f64::max).unwrap();
    println!("Min: {}, Max: {}", min, max);
    
    // Or using min_by/max_by for floats
    let min2 = scores.iter().cloned().fold(f64::INFINITY, f64::min);
    let max2 = scores.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    println!("Min: {}, Max: {}", min2, max2);
}
Sum: 433 Average: 86.60 Min: 78, Max: 92 Min: 78, Max: 92

11 Complete Example: Prime Number Analyzer

This program demonstrates input, output, functions, control structures, and loops working together:

use std::io::{self, Write};

// Helper function to read a line of input
fn read_line(prompt: &str) -> String {
    print!("{}", prompt);
    io::stdout().flush().unwrap();
    
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read line");
    
    input.trim().to_string()
}

// Helper function to read an integer with validation
fn read_positive_int(prompt: &str, min_value: i32) -> i32 {
    loop {
        let input = read_line(prompt);
        match input.parse::<i32>() {
            Ok(n) if n >= min_value => return n,
            Ok(_) => println!("Please enter a number >= {}.", min_value),
            Err(_) => println!("Invalid input. Please enter a number."),
        }
    }
}

// Check if a number is prime
fn is_prime(n: i32) -> bool {
    if n < 2 {
        return false;
    }
    if n == 2 {
        return true;
    }
    if n % 2 == 0 {
        return false;
    }
    
    let mut i = 3;
    while i * i <= n {
        if n % i == 0 {
            return false;
        }
        i += 2;
    }
    true
}

// Find the smallest factor of n (assumes n > 1 and n is not prime)
fn smallest_factor(n: i32) -> i32 {
    if n % 2 == 0 {
        return 2;
    }
    
    let mut i = 3;
    while i * i <= n {
        if n % i == 0 {
            return i;
        }
        i += 2;
    }
    n  // n is prime, return itself
}

// Count prime numbers from 2 to n
fn count_primes_up_to(n: i32) -> i32 {
    let mut count = 0;
    for i in 2..=n {
        if is_prime(i) {
            count += 1;
        }
    }
    count
}

fn main() {
    println!("=== Prime Number Analyzer ===");
    println!();
    
    loop {
        // Get valid input
        let n = read_positive_int("Enter a positive integer (>= 2): ", 2);
        
        // Check if prime and display result
        if is_prime(n) {
            println!("{} is a prime number!", n);
        } else {
            let factor = smallest_factor(n);
            println!("{} is not prime. Its smallest factor is {}.", n, factor);
        }
        
        // Count primes up to n
        let prime_count = count_primes_up_to(n);
        println!("There are {} prime numbers from 2 to {}.", prime_count, n);
        
        // Ask to continue
        println!();
        let response = read_line("Check another number? (y/n): ");
        if response.to_lowercase() != "y" {
            break;
        }
        println!();
    }
    
    println!("Goodbye!");
}

Sample Run

=== Prime Number Analyzer === Enter a positive integer (>= 2): 17 17 is a prime number! There are 7 prime numbers from 2 to 17. Check another number? (y/n): y Enter a positive integer (>= 2): 100 100 is not prime. Its smallest factor is 2. There are 25 prime numbers from 2 to 100. Check another number? (y/n): n Goodbye!

Building and Running

Option 1: Using rustc

rustc prime_analyzer.rs
./prime_analyzer

Option 2: Using Cargo

cargo new prime_analyzer
cd prime_analyzer
# Replace src/main.rs with the code above
cargo run

✏️ Practice Exercises

Exercise 1: Temperature Converter

Write a program that:

  1. Asks the user for a temperature in Fahrenheit
  2. Converts it to Celsius using: C = (F - 32) Ă— 5/9
  3. Displays the result with 2 decimal places
  4. Allows the user to convert multiple temperatures

Exercise 2: Number Guessing Game

Write a program that:

  1. Generates a random number between 1 and 100 (hint: use rand crate)
  2. Asks the user to guess the number
  3. Tells them if their guess is too high, too low, or correct
  4. Counts the number of guesses
  5. Displays the count when they win

Exercise 3: Grade Calculator

Write a program that:

  1. Asks for the number of students
  2. For each student, reads their name and three test scores
  3. Calculates each student's average
  4. Determines their letter grade (A: 90-100, B: 80-89, C: 70-79, D: 60-69, F: below 60)
  5. Displays a formatted table of all students with their averages and grades
  6. Calculates and displays the class average

Exercise 4: Fibonacci Sequence

Write a program that:

  1. Asks the user how many Fibonacci numbers to generate
  2. Generates and displays that many numbers in the sequence
  3. Displays them 10 per line for readability

📝 Starter Template for Exercise 1

use std::io::{self, Write};

// TODO: Create a helper function to read input
fn read_line(prompt: &str) -> String {
    print!("{}", prompt);
    io::stdout().flush().unwrap();
    
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read line");
    
    input.trim().to_string()
}

// TODO: Create a function to convert Fahrenheit to Celsius
fn fahrenheit_to_celsius(f: f64) -> f64 {
    // Formula: C = (F - 32) * 5/9
    _____
}

fn main() {
    println!("=== Temperature Converter ===");
    println!();
    
    loop {
        // TODO: Read temperature from user
        let input = read_line("Enter temperature in Fahrenheit: ");
        
        // TODO: Parse the input to f64
        let fahrenheit: f64 = match input.parse() {
            Ok(n) => n,
            Err(_) => {
                println!("Invalid number. Please try again.");
                continue;
            }
        };
        
        // TODO: Convert and display result
        let celsius = _____;
        println!("{}°F = {:.2}°C", fahrenheit, celsius);
        
        // TODO: Ask if user wants to continue
        println!();
        let response = read_line("Convert another? (y/n): ");
        if response.to_lowercase() != "y" {
            break;
        }
        println!();
    }
    
    println!("Goodbye!");
}

đź“‹ Rust Quick Reference

Task Rust Syntax
Declare immutable variable let x = 5;
Declare mutable variable let mut x = 5;
Declare with type let x: i32 = 5;
Constant const MAX: i32 = 100;
Print with newline println!("Hello");
Print without newline print!("Hello");
Print variable println!("x = {}", x);
Print with formatting println!("{:.2}", 3.14159);
Read line of input io::stdin().read_line(&mut s)
Parse string to number s.trim().parse::<i32>()
If statement if x > 5 { } else { }
Match expression match x { 1 => ..., _ => ... }
Infinite loop loop { break; }
While loop while x > 0 { }
For loop (range) for i in 1..=10 { }
For loop (collection) for item in &vec { }
Function definition fn name(x: i32) -> i32 { }
Create empty vector let mut v: Vec<i32> = Vec::new();
Create vector with values let v = vec![1, 2, 3];
Add to vector v.push(4);
Vector length v.len()
Access vector element v[0] or v.get(0)

đź“‹ Common Cargo Commands

Command Description
cargo new project_name Create a new Rust project
cargo build Compile the project (debug mode)
cargo build --release Compile with optimizations
cargo run Build and run the project
cargo run -- arg1 arg2 Run with command-line arguments
cargo check Check for errors without building
cargo clean Remove compiled artifacts
cargo test Run unit tests
cargo fmt Format code according to style guidelines
cargo clippy Run linter for additional warnings
rustc file.rs Compile single file without Cargo

đź“‹ Rust vs Pascal Comparison

Concept Rust Pascal
Program entry fn main() { } begin ... end.
Variable declaration let x: i32 = 5; var x: integer;
Constants const X: i32 = 5; const X = 5;
Print with newline println!("text"); writeln('text');
Print without newline print!("text"); write('text');
Read input io::stdin().read_line(&mut s) readln(s);
Assignment x = 5; x := 5;
Equality test x == 5 x = 5
Not equal x != 5 x <> 5
Logical AND && and
Logical OR || or
Logical NOT ! not
If statement if cond { } else { } if cond then ... else ...
For loop for i in 1..=10 { } for i := 1 to 10 do
While loop while cond { } while cond do
Repeat/loop until loop { if cond { break; } } repeat ... until cond;
Function fn name() -> i32 { } function name: integer;
Procedure fn name() { } procedure name;
Comments // line or /* block */ { block } or (* block *)
Integer division a / b (for integers) a div b
Modulus a % b a mod b
Case sensitivity Case-sensitive Case-insensitive

đź“‹ Common Types

Rust Type Size Pascal Equivalent
i8 8-bit signed ShortInt
i16 16-bit signed SmallInt
i32 32-bit signed Integer / LongInt
i64 64-bit signed Int64
u8 8-bit unsigned Byte
u16 16-bit unsigned Word
u32 32-bit unsigned Cardinal / LongWord
u64 64-bit unsigned QWord
f32 32-bit float Single
f64 64-bit float Double / Real
bool Boolean Boolean
char Unicode (4 bytes) Char (1 byte)
String Growable string String
&str String slice N/A
Vec<T> Dynamic array array of T
[T; N] Fixed array array[0..N-1] of T
⚠️ Common Rust Mistakes to Avoid:
đź’ˇ Pro Tips:

đź“‹ Format Specifiers

Specifier Description Example Output
{} Default display println!("{}", 42); 42
{:?} Debug format println!("{:?}", vec![1,2]); [1, 2]
{:#?} Pretty debug println!("{:#?}", vec![1,2]); Multi-line
{:.2} 2 decimal places println!("{:.2}", 3.14159); 3.14
{:8} Width 8, right-aligned println!("{:8}", 42); 42
{:<8} Width 8, left-aligned println!("{:<8}", 42); 42
{:^8} Width 8, centered println!("{:^8}", 42); 42
{:08} Zero-padded width 8 println!("{:08}", 42); 00000042
{:b} Binary println!("{:b}", 42); 101010
{:x} Hexadecimal (lower) println!("{:x}", 255); ff
{:X} Hexadecimal (upper) println!("{:X}", 255); FF
{:o} Octal println!("{:o}", 64); 100
{:e} Scientific notation println!("{:e}", 1234.5); 1.2345e3

✏️ Self-Check Questions

  1. What is the difference between let x = 5; and let mut x = 5;?
  2. Why do we use .trim() after reading input with read_line?
  3. What is the difference between 1..5 and 1..=5?
  4. Why does Rust require explicit type conversion (casting) between numeric types?
  5. What happens if you try to access an array index that doesn't exist?
  6. What is the difference between println! and print!?
  7. Why is a + b different from a + b; at the end of a function?
  8. What does the _ pattern match in a match expression?
  9. How do you break out of a nested loop to an outer loop?
  10. What is the difference between a String and a &str?
Click to reveal answers
  1. let x = 5; creates an immutable variable that cannot be changed. let mut x = 5; creates a mutable variable that can be reassigned.
  2. read_line includes the newline character (\n) that the user pressed. .trim() removes whitespace including this newline, which is necessary before parsing to a number.
  3. 1..5 is an exclusive range (1, 2, 3, 4). 1..=5 is an inclusive range (1, 2, 3, 4, 5).
  4. Explicit type conversion prevents subtle bugs that can occur with implicit conversions, such as unexpected precision loss or overflow behavior.
  5. Rust will panic (crash) with an "index out of bounds" error. This is safer than C/C++ which would access invalid memory.
  6. println! adds a newline after the output. print! does not, so subsequent output appears on the same line.
  7. Without a semicolon, a + b is an expression that returns a value (the sum). With a semicolon, a + b; is a statement that evaluates and discards the result, returning () (unit/nothing).
  8. _ is the catch-all or wildcard pattern that matches anything not explicitly handled by other arms.
  9. Use labeled loops: 'outer: for ... { for ... { break 'outer; } }
  10. String is an owned, growable, heap-allocated string. &str is a borrowed reference to string data (a "string slice") that can point to a String or a string literal.