Series: Learn Rust from Scratch | Post 3 of 11
Introduction
Every meaningful program needs two things: functions (reusable blocks of logic) and control flow (the ability to make decisions and repeat actions). In this post, we'll master both.
Rust handles these concepts in interesting ways — if is an expression, not just a statement, and functions have a clean and predictable return syntax. Let's explore.
Functions
You've already seen one function: main. Let's write our own.
Defining a Function
// Define a function using the `fn` keyword
fn greet() {
println!("Hello from a function!");
}
fn main() {
// Call the function
greet();
greet(); // Functions can be called multiple times
}
Output:
Hello from a function!
Hello from a function!
Naming convention: Function names in Rust use
snake_case— lowercase letters with underscores between words.
Functions with Parameters
Parameters let you pass information into a function. In Rust, you must declare the type of each parameter:
// This function takes two parameters
fn introduce(name: &str, age: u32) {
println!("Hi! I'm {} and I'm {} years old.", name, age);
}
fn add(a: i32, b: i32) {
println!("{} + {} = {}", a, b, a + b);
}
fn main() {
introduce("Neo", 25);
introduce("Alice", 30);
add(10, 20);
add(7, 3);
}
Output:
Hi! I'm Neo and I'm 25 years old.
Hi! I'm Alice and I'm 30 years old.
10 + 20 = 30
7 + 3 = 10
Functions That Return Values
Use -> to specify the return type. The last expression in a function is automatically returned — no return keyword needed (though you can use it for early returns).
// Returns an i32
fn square(n: i32) -> i32 {
n * n // No semicolon → this is the return value (an expression)
}
// Returns a f64
fn circle_area(radius: f64) -> f64 {
std::f64::consts::PI * radius * radius
}
// Using explicit `return` for early exit
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
return 0.0; // Early return with semicolon
}
a / b // Normal return — last expression, no semicolon
}
fn main() {
let result = square(5);
println!("5 squared = {}", result);
let area = circle_area(3.0);
println!("Circle area (r=3): {:.2}", area);
println!("10 / 2 = {}", divide(10.0, 2.0));
println!("10 / 0 = {}", divide(10.0, 0.0));
}
Output:
5 squared = 25
Circle area (r=3): 28.27
10 / 2 = 5
10 / 0 = 0
Key Rule: A line without a semicolon at the end of a function body is the return value. A line with a semicolon is a statement that doesn't return anything.
Functions Returning Multiple Values (via Tuples)
// Return two values as a tuple
fn min_max(numbers: &[i32]) -> (i32, i32) {
let mut min = numbers[0];
let mut max = numbers[0];
for &num in numbers {
if num < min { min = num; }
if num > max { max = num; }
}
(min, max) // Return a tuple
}
fn main() {
let data = [3, 7, 1, 9, 2, 8, 4];
let (minimum, maximum) = min_max(&data); // Destructure the tuple
println!("Min: {}, Max: {}", minimum, maximum);
}
Output:
Min: 1, Max: 9
Control Flow
if / else if / else
fn classify_temperature(temp: f64) {
if temp < 0.0 {
println!("Freezing!");
} else if temp < 15.0 {
println!("Cold.");
} else if temp < 25.0 {
println!("Comfortable.");
} else if temp < 35.0 {
println!("Warm.");
} else {
println!("Hot!");
}
}
fn main() {
classify_temperature(-5.0);
classify_temperature(10.0);
classify_temperature(22.0);
classify_temperature(40.0);
}
Output:
Freezing!
Cold.
Comfortable.
Hot!
if as an Expression
In Rust, if is an expression, which means it can return a value. This replaces the ternary operator (? :) found in other languages.
fn main() {
let number = 7;
// Assign based on condition — both arms must return the same type
let description = if number % 2 == 0 { "even" } else { "odd" };
println!("{} is {}", number, description);
// Use it directly in a function call
let score = 85;
println!("Grade: {}", if score >= 90 { "A" } else if score >= 75 { "B" } else { "C" });
// Assign a computed value
let abs_value = if number >= 0 { number } else { -number };
println!("Absolute value: {}", abs_value);
}
Output:
7 is odd
Grade: B
Absolute value: 7
Important: When using
ifas an expression, both theifandelsebranches must return the same type. If one returns ani32and the other returns a&str, it won't compile.
Loops
Rust has three loop constructs: loop, while, and for.
loop — Loop Forever (Until You Break)
loop creates an infinite loop. Use break to exit and optionally return a value.
fn main() {
let mut counter = 0;
// loop returns a value via `break`
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Break and return a value
}
};
println!("Counter stopped at {}, result = {}", counter, result);
// A simple countdown
let mut countdown = 5;
loop {
println!("{}...", countdown);
countdown -= 1;
if countdown == 0 {
println!("Liftoff! ");
break;
}
}
}
Output:
Counter stopped at 10, result = 20
5...
4...
3...
2...
1...
Liftoff!
while — Loop While a Condition Is True
fn main() {
// Countdown with while
let mut number = 3;
while number > 0 {
println!("{}!", number);
number -= 1;
}
println!("GO!");
// Collect digits of a number
let mut n = 12345;
let mut digits = Vec::new();
while n > 0 {
digits.push(n % 10); // Get the last digit
n /= 10; // Remove the last digit
}
digits.reverse();
println!("Digits: {:?}", digits);
}
Output:
3!
2!
1!
GO!
Digits: [1, 2, 3, 4, 5]
for — Loop Over a Collection (The Preferred Loop)
for is the most common and idiomatic loop in Rust. It's safer than while because you can't accidentally go out of bounds.
fn main() {
// Loop over an array
let planets = ["Mercury", "Venus", "Earth", "Mars"];
for planet in planets {
println!("Planet: {}", planet);
}
// Loop over a range (1 to 5, exclusive of 6)
for i in 1..=5 { // 1..=5 is inclusive; 1..5 excludes 5
println!("i = {}", i);
}
// Loop with index using enumerate()
let fruits = ["apple", "banana", "cherry"];
for (index, fruit) in fruits.iter().enumerate() {
println!("{}: {}", index + 1, fruit);
}
// Sum numbers from 1 to 100
let mut sum = 0;
for n in 1..=100 {
sum += n;
}
println!("Sum 1-100 = {}", sum);
}
Output:
Planet: Mercury
Planet: Venus
Planet: Earth
Planet: Mars
i = 1
i = 2
i = 3
i = 4
i = 5
1: apple
2: banana
3: cherry
Sum 1-100 = 5050
continue — Skip the Rest of This Iteration
fn main() {
// Print only odd numbers from 1 to 10
for n in 1..=10 {
if n % 2 == 0 {
continue; // Skip even numbers
}
println!("{}", n);
}
}
Output:
1
3
5
7
9
Loop Labels — Breaking Out of Nested Loops
fn main() {
let mut found = false;
// Label the outer loop with 'outer:
'outer: for i in 0..5 {
for j in 0..5 {
if i + j == 6 {
println!("Found i={}, j={} where i+j=6", i, j);
found = true;
break 'outer; // Break the OUTER loop, not just the inner
}
}
}
if found {
println!("Search complete.");
}
}
Output:
Found i=2, j=4 where i+j=6
Search complete.
Putting It All Together: A Complete Example
Let's build a small program that combines functions, conditions, and loops:
/// Checks 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
}
/// Returns the Fibonacci number at position n
fn fibonacci(n: u32) -> u64 {
if n <= 1 {
return n as u64;
}
let mut a: u64 = 0;
let mut b: u64 = 1;
for _ in 2..=n { // `_` ignores the loop variable
let temp = a + b;
a = b;
b = temp;
}
b
}
fn main() {
// Find all primes up to 50
println!("Primes up to 50:");
let primes: Vec<u32> = (2..=50).filter(|&n| is_prime(n)).collect();
println!("{:?}", primes);
// Print first 10 Fibonacci numbers
println!("\nFirst 10 Fibonacci numbers:");
for i in 0..10 {
print!("{} ", fibonacci(i));
}
println!();
}
Output:
Primes up to 50:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
First 10 Fibonacci numbers:
0 1 1 2 3 5 8 13 21 34
Summary
In this post, you learned:
- ✅ How to define functions with
fn, parameters, and return types (->) - ✅ The last expression in a function is returned automatically (no semicolon)
- ✅
ifis an expression in Rust — it can return a value - ✅
loopcreates an infinite loop;breakcan return a value from it - ✅
whileloops until a condition is false - ✅
foriterates over collections or ranges (preferred for safety) - ✅
continueskips an iteration; loop labels let you break outer loops
What's Next?
In Post 4, we'll dive into the most unique aspect of Rust: Ownership, Borrowing, and References. This is what makes Rust different from every other language — and it's where the real learning begins!
Next Post: Ownership, Borrowing, and References in Rust →
Part of the Learn Rust from Scratch series on CodeWithNeo


Leave a Reply