Functions in Rust
About This Module
This module covers Rust function syntax, return values, parameters, and the unit type. Functions are fundamental building blocks in Rust programming, and understanding their syntax and behavior is essential for writing well-structured Rust programs.
Prework
Prework Readings
Read the following sections from "The Rust Programming Language" book:
- Chapter 3.3: Functions
- Chapter 4.1: What Is Ownership? - Introduction only
Pre-lecture Reflections
Before class, consider these questions:
- How do functions help organize and structure code?
- What are the benefits of explicit type annotations in function signatures?
- How do return values differ from side effects in functions?
- What is the difference between expressions and statements in function bodies?
- How might Rust's approach to functions differ from other languages you know?
Learning Objectives
By the end of this module, you should be able to:
- Define functions with proper Rust syntax
- Understand parameter types and return type annotations
- Use both explicit
returnstatements and implicit returns - Work with functions that return no value (unit type)
- Apply best practices for function design and readability
- Understand the difference between expressions and statements in function bodies
Function Syntax
Syntax:
fn function_name(argname_1:type_1,argname_2:type_2) -> type_ret {
DO-SOMETHING-HERE-AND-RETURN-A-VALUE
}
- No need to write "return x * y"
- Last expression is returned
- No semicolon after the last expression
fn multiply(x:i32, y:i32) -> i32 { // note: no need to write "return x * y" x * y } fn main() { println!("{}", multiply(10,20)) }
Exercise: Try putting a semicolon after the last expression. What happens?
Functions returns
- But if you add a return then you need a semicolon
- unless it is the last statement in the function
- Recommend using returns and add semicolons everywhere.
- It's easier to read.
fn and(p:bool, q:bool, r:bool) -> bool { if !p { println!("p is false"); return false; } if !q { println!("q is false"); return false; } println!("r is {}", r); r // return r without the semicolon also works here } fn main() { println!("{}", and(true,false,true)) }
Functions: returning no value
How: skip the type of returned value part
fn say_hello(who:&str) { println!("Hello, {}!",who); } fn main() { say_hello("world"); say_hello("Boston"); say_hello("DS210"); }
Nothing returned equivalent to the unit type, ()
fn say_good_night(who:&str) -> () { println!("Good night {}",who); } fn main() { say_good_night("room"); say_good_night("moon"); let z = say_good_night("cow jumping over the moon"); println!("The function returned {:?}", z) }
Unit Type Characteristics:
- Empty tuple:
() - Zero size: Takes no memory
- Default return: When no value is explicitly returned
- Side effects only: Functions that only perform actions (printing, file I/O, etc.)
Parameter Handling
Multiple Parameters:
#![allow(unused)] fn main() { fn calculate_area(length: f64, width: f64) -> f64 { length * width } fn greet_person(first_name: &str, last_name: &str, age: u32) { println!("Hello, {} {}! You are {} years old.", first_name, last_name, age); } }
Parameter Types:
- Ownership: Parameters can take ownership (
String) - References: Parameters can borrow (
&str,&i32) - Primitive types: Copied by default (
i32,bool,f64)
Function Design Principles
Single Responsibility:
// Good: Single purpose fn calculate_tax(price: f64, tax_rate: f64) -> f64 { price * tax_rate } // Good: Clear separation of concerns fn format_currency(amount: f64) -> String { format!("${:.2}", amount) } fn display_total(subtotal: f64, tax_rate: f64) { let tax = calculate_tax(subtotal, tax_rate); let total = subtotal + tax; println!("Total: {}", format_currency(total)); } fn main() { display_total(100.0, 0.08); }
Pure Functions vs. Side Effects:
#![allow(unused)] fn main() { // Pure function: No side effects, deterministic fn add(x: i32, y: i32) -> i32 { x + y } // Function with side effects: Prints to console fn add_and_print(x: i32, y: i32) -> i32 { let result = x + y; println!("{} + {} = {}", x, y, result); result } }
Common Patterns
Validation Functions:
#![allow(unused)] fn main() { fn is_valid_age(age: i32) -> bool { age >= 0 && age <= 150 } fn is_valid_email(email: &str) -> bool { email.contains('@') && email.contains('.') } }
Conversion Functions:
#![allow(unused)] fn main() { fn celsius_to_fahrenheit(celsius: f64) -> f64 { celsius * 9.0 / 5.0 + 32.0 } fn fahrenheit_to_celsius(fahrenheit: f64) -> f64 { (fahrenheit - 32.0) * 5.0 / 9.0 } }
Helper Functions:
#![allow(unused)] fn main() { fn get_absolute_value(x: i32) -> i32 { if x < 0 { -x } else { x } } fn max_of_three(a: i32, b: i32, c: i32) -> i32 { if a >= b && a >= c { a } else if b >= c { b } else { c } } }
Function Naming Conventions
Rust Naming Guidelines:
- snake_case: For function names
- Descriptive names: Clear indication of purpose
- Verb phrases: For functions that perform actions
- Predicate functions: Start with
is_,has_,can_
Examples:
#![allow(unused)] fn main() { fn calculate_distance(x1: f64, y1: f64, x2: f64, y2: f64) -> f64 { /* ... */ } fn is_prime(n: u32) -> bool { /* ... */ } fn has_permission(user: &str, resource: &str) -> bool { /* ... */ } fn can_access(user_level: u32, required_level: u32) -> bool { /* ... */ } }
Exercise
Write a function called greet_user that takes a name and a time of day (morning, afternoon, evening) as parameters and returns an appropriate greeting string.
The function should:
- Take two parameters:
name: &strandtime: &str - Return a
Stringwith a customized greeting - Follow Rust naming conventions
- Use proper parameter types
- Include error handling for invalid times
Example output:
Good evening, Dumbledore!
Hint: You can format the string using the format! macro, which uses the same syntax as println!.
// Returns a String
format!("Good morning, {}!", name)
// Your code here