Jump to content

Loops

From JOHNWICK
Revision as of 18:59, 23 November 2025 by PC (talk | contribs) (Created page with "Loops in programming (fundamental and very useful), including Rust, are like a repeating task you tell the computer to do until a certain condition is met or a task is finished. Think of loops as asking someone to keep stirring a pot of soup until it’s ready. Yesterday we covered iterators, and if you missed that you can check it out below. Iterators After playing with vectors, and enums the last two days, it’s time to look in on Rust’s iterators -a feature that ma...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Loops in programming (fundamental and very useful), including Rust, are like a repeating task you tell the computer to do until a certain condition is met or a task is finished. Think of loops as asking someone to keep stirring a pot of soup until it’s ready. Yesterday we covered iterators, and if you missed that you can check it out below. Iterators After playing with vectors, and enums the last two days, it’s time to look in on Rust’s iterators -a feature that makes… medium.com

Rust provides three main loop constructs: loop, while, and for. Each with its own strengths, and useful for different situations.

Loops can be useful whether you’re parsing logs, iterating over database records, or retrying network requests. In this writeup we will look at Loops in Rust in-depth.

The loop Construct

The loop keyword creates an infinite loop that runs until you explicitly stop it with break. This makes loops perfect for scenarios where you need to keep trying something until a condition is met, like polling a server or waiting for user input. Imagine we’re building a command-line tool that monitors a server’s status by sending HTTP requests until it gets a successful response. Below is how we might use loop to handle this:

use std::time::Duration;
use std::thread;

fn check_server_status() -> bool {
    // Simulate a server check; returns true if server is up
    // In a real app, this might be an HTTP request
    rand::random::<bool>()
}
fn main() {
    let mut attempts = 0;
    let max_attempts = 5;

    loop {
        if attempts >= max_attempts {
            println!("Failed to connect after {} attempts.", max_attempts);
            break;
        }
        if check_server_status() {
            println!("Server is up!");
            break;
        }
        println!("Attempt {} failed. Retrying...", attempts + 1);
        attempts += 1;
        thread::sleep(Duration::from_secs(1));
    }
}

Take a look at the code above, the loop keeps trying to connect to the server until it either succeeds or hits the maximum number of attempts. The break statement is your escape hatch, it lets you exit cleanly when the job is done or when you’ve tried too many times. It is a common pattern in system utilities or monitoring tools where you need to retry operations.

You can also use loop to return a value by adding an expression after break. Suppose you’re parsing a log file line by line until you find a specific error code, and you want to return the line number where it was found:

fn find_error_line(logs: &[&str], error_code: &str) -> Option<usize> {
    let mut line_number = 0;
    loop {
          if line_number >= logs.len() {
              return None;
          }
          if logs[line_number].contains(error_code) {
              break Some(line_number);
          }
          line_number += 1;
     }
}

fn main() {
    let logs = vec!["INFO: System started", "ERROR: 404 Not Found", "INFO: System shutdown"];
    
    match find_error_line(&logs, "404") {
        Some(line) => println!("Error found at line {}", line + 1),
        None => println!("No error found"),
    }
}

The code above loop searches through a vector of log entries and returns the index of the first line containing the error code, or None if it’s not found. It’s a pattern you might find useful when analyzing log files or processing data streams in a backend service.

Now that we have looked at loop, let’s look at while loop.

The while Loop

The while loop runs as long as a condition is true, making it ideal for situations where you know when to stop based on some state. You’ll often see while in scenarios where you’re processing data until a resource is exhausted or a condition changes.

Consider a scenario where you’re reading messages from a queue in a messaging system, stopping when the queue is empty or an error occurs:

struct MessageQueue {
    messages: Vec<String>,
}

impl MessageQueue {
    fn pop(&mut self) -> Option<String> {
        self.messages.pop()
    }
}

fn process_messages(queue: &mut MessageQueue) {
    while let Some(message) = queue.pop() {
        println!("Processing message: {}", message);
        // we could Simulate processing here; 
        // could be saving to a database or 
        // sending to another service
    }
    println!("Queue is empty!");
}

fn main() {
    let mut queue = MessageQueue {
        messages: vec![
            String::from("User logged in"),
            String::from("User updated profile"),
            String::from("User logged out"),
        ],
    };
    process_messages(&mut queue);
}

In our snippet above, while let is used to keep processing messages until the queue is empty. The pop method returns None when there are no more messages, which stops the loop. This pattern is common in systems that handle event streams, like message brokers or task queues in a microservices architecture.

Another real-world use case is validating user input. Suppose you’re writing a CLI tool that asks for a valid email address:

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

fn is_valid_email(email: &str) -> bool {
    email.contains('@') // Simplified validation
}

fn get_valid_email() -> String {
    let mut email = String::new();
    while !is_valid_email(&email) {
        print!("Enter a valid email address: ");
        io::stdout().flush().unwrap();
        email.clear();
        io::stdin().read_line(&mut email).unwrap();
        email = email.trim().to_string();
    }
    email
}

fn main() {
    let email = get_valid_email();
    println!("Valid email entered: {}", email);
}

The while loop keeps prompting until the user provides a valid email. This is a typical pattern in interactive applications where you need to ensure input meets certain criteria before proceeding.

The for Loop

The for loop is your go-to for iterating over collections, like arrays, vectors, or ranges. It’s clean and expressive, especially when paired with Rust’s iterators, which let you process data in a functional style. You’ll use for loops when you need to process every item in a dataset, like generating reports or transforming data.

Let’s say you’re working on a data processing pipeline that calculates the total sales from a list of transactions:

struct Transaction {
    amount: f64,
    customer_id: u32,
}

fn calculate_total_sales(transactions: &[Transaction]) -> f64 {
    let mut total = 0.0;
    for transaction in transactions {
        total += transaction.amount;
    }
    total
}

fn main() {
    let transactions = vec![
        Transaction { amount: 100.50, customer_id: 1 },
        Transaction { amount: 200.75, customer_id: 2 },
        Transaction { amount: 50.25, customer_id: 1 },
    ];
    let total = calculate_total_sales(&transactions);
    println!("Total sales: ${:.2}", total);
}

In our code above, the for loop iterates over a slice of Transaction structs, summing their amounts. This is a common task in financial applications or e-commerce platforms where you need to aggregate data.

Rust’s for loop also shines with ranges, which are handy for tasks like generating sequences or retrying operations a fixed number of times. Suppose you’re writing a load tester that sends a fixed number of requests to a server:

fn send_request(id: u32) -> bool {
    // we could simulate a request; returns true if successful
    println!("Sending request {}", id);
    true
}

fn main() {
    for i in 1..=5 {
        if send_request(i) {
            println!("Request {} succeeded", i);
        } else {
            println!("Request {} failed", i);
        }
    }
}

The 1..=5 range includes numbers from 1 to 5, making the loop iterate exactly five times. This is useful in testing tools or scripts where you need to perform a task a specific number of times.

Choosing the Right Loop

Each loop type has its own strengths, and useful for different situations.

  • Use loop for infinite or unpredictable tasks, like retrying network calls or polling.
  • Use while when you have a condition that determines when to stop, like processing a queue or validating input.
  • Use for for iterating over collections or fixed ranges, like processing data or generating sequences.

In real world scenarios, you’ll often combine loops with Rust’s ownership and borrowing rules to ensure memory safety. For example, when iterating over a vector, you might borrow it as a slice (&vec) to avoid moving it, or use &mut if you need to modify elements (we covered this on vectors yesterday).

Loops with Iterators

Rust’s iterators (as we saw yesterday) can supercharge your for loops, letting you chain operations like filtering or mapping. Imagine you’re filtering out invalid transactions from a dataset before summing them:

fn calculate_valid_sales(transactions: &[Transaction]) -> f64 {
    transactions
        .iter()
        .filter(|t| t.amount > 0.0)
        .map(|t| t.amount)
        .sum()
}

fn main() {
    let transactions = vec![        Transaction { amount: 100.50, customer_id: 1 },        Transaction { amount: -10.0, customer_id: 2 }, // Invalid        Transaction { amount: 50.25, customer_id: 1 },    ];
    let total = calculate_valid_sales(&transactions);
    println!("Total valid sales: ${:.2}", total);
}

The code above uses a for-like iterator chain to filter out negative amounts and sum the rest, all in a concise, readable way. You’ll see this pattern in data processing pipelines or analytics tools where you need to transform and aggregate data efficiently.

Common Pitfalls and Tips

Below are some common pitfalls to look out for and avoid while working with loops in Rust.

  • Be cautious with loop to avoid infinite loops. Always ensure there’s a break condition, like a counter or a timeout.
  • When using for with mutable data, ensure you’re borrowing correctly. Use & for read-only access or &mut for modifications.
  • When chaining iterators, remember that methods like collect consume the iterator, so plan your data flow to avoid moving data unexpectedly.
  • For large datasets, prefer iterators over manual indexing in while loops to take advantage of Rust’s optimizations.

Read the full article here: https://medium.com/rustaceans/loops-e2026244a069