Lecture 8 - Functions in Rust
Logistics
- Coffee slots today
- HW2 due Wednesday
- The first midterm is in two weeks
Follow-up to yesterday
- 2's complement (at the board) (but also, don't worry about it)
- The
let x:u8 = 5; let y = -x;error - Going over the shakespeare problem
Learning Objectives
By the end of this lecture, students should be able to:
- Write function signatures including parameter names, types, and return types
- Create functions that return the unit type
()for side-effect-only operations - Explain the difference between an expression and a statement in Rust
- Use expressions to assign values based on conditions
Function Syntax
We've seen lots of examples like this:
#![allow(unused)] fn main() { fn my_age_in_5_years(age: i16) -> i16 { let new_age = age + 5; return age; // you can just put "age" without return but "return" is clearer } }
General function template:
#![allow(unused)] fn main() { fn function_name(arg_name_1:arg_type_1,arg_name_2:arg_type_2) -> type_returned // ^ This part is the "function signature" { // Do stuff // return something } // ^ This part is the "function body" and can be a statement or expression }
Statements and expressions
Just as in math when we have:
- expressions like (\(a^2 + b^2)\)
- and equations like (\(a^2 + b^2 = c^2)\)
In rust we have expressions and statements
- Expressions simplify to a value (like a math expression)
- Statements do things but don't simplify to a value (kind of like an equation?)
So -
y + 2is an expressionlet x = y + 2;is a statement
Statements and expressions can be nested
let x = y + 2; is a statement BUT it INCLUDES y + 2 which is an expression
The reverse is also true - we can build complex expressions that include statements
#![allow(unused)] fn main() { let y = { let x = 2 * 3; x }; }
A statement or expression - shout it out
#![allow(unused)] fn main() { let x = 5; // Statement or expression? x + 2 // Statement or expression? println!("hello"); // Statement or expression? my_function(5) // Statement or expression? let y = x + 2; // Statement or expression? { let z = 10; // Statement or expression? z * 2 // Statement or expression? } // Statement or expression? return x + 5; // Statement or expression? let x = { println!("doing work"); // Statement or expression? 42 // Statement or expression? }; // Statement or expression? }
Maybe it was too easy to cheat because...
- Statements always end with semi-colons
- Expressions never end with semi-colons
Key insight: {} blocks are expressions that evaluate to their final line (if no semicolon).
Adding a semicolon turns an expression into a statement
fn main(){ let a = { let x = 10; x + 5 // Expression }; println!("{}",a); let b = { let x = 10; x + 5; // Statement }; println!("{}",b); }
Let's look at return again now
We have two ways of returning from a function:
#![allow(unused)] fn main() { fn my_age_in_5_years(age: i16) -> i16 { let new_age = age + 5; return new_age; } }
#![allow(unused)] fn main() { fn my_age_in_5_years(age: i16) -> i16 { let new_age = age + 5; new_age } }
Why are these effectively the same thing?
But what happens if you don't return anything?
fn say_hello(who:&str) { // no -> return_type here // fn say_hello(who:&str) -> () { println!("Hello, {}!",who); } fn main() { say_hello("world"); say_hello("Boston"); say_hello("DS210"); // let z = say_hello("DS210"); // println!("The function returned {:?}", z) }
Functions that return no value
Functions that don't return or end in an expression return "the unit type" ()
() is an empty tuple that takes no memory (think of an empty set!)
This lets us have "side-effects only" functions that perform actions (printing, file I/O, etc.)
Passing parameters
3 ways to pass parameters
- A parameter can be copied into a function (default for
i32,bool,f64, other basic types) - A function can take ownership of a parameter (default for
String, other complex types) - A function can borrow a parameter to "peek" at it without "owning" it (
&str,&i32)
Examples:
#![allow(unused)] fn main() { fn greet_person(first_name: String, last_name: &str, age: u32) { // first_name now OWNS what was passed to it // last_name is BORROWING what was passed to it // age COPIED what was passed to it println!("Hello, {} {}! You are {} years old.", first_name, last_name, age); } }
We'll talk a lot more about owning vs borrowing later. For now, some simple rules to get started:
Quick Rules for Beginners:
- Use
&strfor string parameters (lets you pass in any string without taking it) - Use
&before the parameter name when you want to "peek" at data without taking it - Basic types like
i32,f64,boolare automatically copied - no worries there - If Rust complains about ownership, try adding
&to your parameter type - You typically can't use a reference (
&) in a return value - thats why you'll seeStringas a return type in HW2
Examples:
#![allow(unused)] fn main() { fn print_name(name: &str) { /* name is borrowed - original still usable */ } fn calculate_area(width: f64, height: f64) -> f64 { /* both copied */ } }
Lecture 7 Review Quiz
Take 2 minutes with a partner to discuss these questions and I'll call on you
- Can you change
xto a different type usingmut? Using shadowing? - What's the largest value a
u8can hold? - What are three different changes you could make so that this compiles?
#![allow(unused)] fn main() { let x: i32 = 10; let y: i16 = 5; let sum = x + y; }
- What's wrong with this?
const PI = 3.14;
Function Design Principles
Best Practice - Single Responsibility
#![allow(unused)] fn main() { // Good: Single purpose fn calculate_cook_time(base_time: u32, servings: u32) -> u32 { base_time + (servings * 2) } // Good: Clear separation of concerns fn format_time(minutes: u32) -> String { if minutes >= 60 { format!("{}h {}m", minutes / 60, minutes % 60) } else { format!("{}m", minutes) } } fn display_recipe_info(base_cook_time: u32, servings: u32) { let total_time = calculate_cook_time(base_cook_time, servings); println!("Cook time for {} servings: {}", servings, format_time(total_time)); } }
Common Patterns - Pure Functions vs. Side Effects
#![allow(unused)] fn main() { // Pure function: No side effects 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('.') } }
Common Patterns - 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 } }
Common Patterns - Helper Functions
#![allow(unused)] fn main() { fn get_absolute_value(x: i32) -> i32 { if x < 0 { -x } else { x } } }
Function Naming Conventions
Rust Naming Guidelines:
- snake_case
- Descriptive names that indicate purpose
- Verb phrases for functions that perform actions
- Predicate phrases for functions that return booleans (
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 { /* ... */ } }
Last thing - Using if Statements (TC 12:45)
We've glossed over this so far - let's get into some details
Syntax:
#![allow(unused)] fn main() { if condition { // } else if { // } else { // } }
else ifandelseparts optional
Example of if in a function
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); return r; } fn main() { println!("{}", and(true, false, true)); }
Best Practices for if statements
- Use consistent indentation (4 spaces or tabs)
- Keep conditions readable - use parentheses for clarity when needed
- Prefer early returns in functions to reduce nesting
- Use
else iffor multiple conditions rather than nestedif
Example of Good Style:
#![allow(unused)] fn main() { fn classify_grade(score: f64) -> char { if score > 90.0 { 'A' } else if score > 80.0 { 'B' } else if score > 70.0 { 'C' } else { 'D' } } }
Even though Rust doesn't require tabs like this it's still a good idea for readability!
Bringing it together with expressions
You can even use conditional expressions as values!
Python:
x = 100 if (x == 7) else 200
Rust:
#![allow(unused)] fn main() { let x = 4; let z = if x == 7 {100} else {200}; println!("{}",z); }
// won't work: same type needed fn main(){ let x = 4; println!("{}",if x == 7 {100} else {1.2}); }
But don't do it just because you can
#![allow(unused)] fn main() { // This is technically valid but TERRIBLE code please DO NOT DO THIS let x = 4; let result = if x > 0 { if x < 10 { let temp = x * x; let bonus = if temp > 10 { 5 } else { 2 }; temp + bonus } else { let factor = x / 2; if factor > 3 { factor * 3 } else { factor + 1 } } } else { 0 }; println!("Result: {}", result); }