Lecture 7 - Functions: parameters, returns, and expressions
Announcements
Learning Objectives
By the end of this lecture, you should be able to:
- Write function signatures including parameter names, types, and return types
- Return more than one value from a function by returning a tuple
- Create functions that return the unit type
()for side-effect-only operations - Explain the difference between an expression and a statement in Rust
- Pass parameters into functions via copying, borrowing, and passing ownership
Function Syntax
We've seen a few examples like this:
#![allow(unused)] fn main() { fn my_age_in_5_years(age: i16) -> i16 { let new_age = age + 5; return new_age; } }
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 inside }
Where you put it does not matter. Rust sees every fn in the file wherever it sits, so main can call a function written below it (couldn't in Python!)
The signature is the function's "promise"
#![allow(unused)] fn main() { /// Returns the distance between two points on a line. fn distance(a: f64, b: f64) -> f64 { (a - b).abs() } }
The signature tells you what goes in and what comes out.
The /// above it is called a "doc comment" or "docstring" and it tells you what the function is for. (Your editor keeps the /// going when you press Enter)
Naming a function is naming its contract. If you can't keep it brief, the function is probably doing more than one job!
You will run into other comment-looking things in Rust code: /* */, /** */, //!. You don't need any of them in this course, and you can look them up if you see them.
Statements and expressions
Just as in math when we have:
- expressions like ()
- and equations like ()
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
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 semicolons
- Expressions never end with semicolons
So {} blocks are expressions too. They evaluate to their final line, as long as it has 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); }
That little {:?} makes things that don't normally print, print anyway! It's called "debug printing" and we'll see it more later.
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; } }
We can also:
#![allow(unused)] fn main() { fn my_age_in_5_years(age: i16) -> i16 { let new_age = age + 5; new_age } }
T/P/S - Why are these effectively the same thing? (Hint: think about expressions and statements)
Returning more than one thing
A function returns one value. But that one value can be a tuple, which groups several values together:
/// Returns the smallest and largest of three numbers. fn min_max(a: i32, b: i32, c: i32) -> (i32, i32) { let smallest = a.min(b).min(c); let largest = a.max(b).max(c); (smallest, largest) } fn main() { let (lo, hi) = min_max(14, 3, 27); println!("range: {} to {}", lo, hi); let t = min_max(14, 3, 27); println!("range: {} to {}", t.0, t.1); println!("{:?}", t); }
(i32, i32) is the return type: two integers, in that order.
Then we can unpack with let (lo, hi) = ... to split it back into two names.
You can reach into a tuple by position with .0 and .1, and {:?} prints the whole tuple at once.
But what happens if you don't return anything?
fn say_hello(who:&str) { // no -> return_type here // vs 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.)
Pure, or side effects?
#![allow(unused)] fn main() { // Pure: same inputs, same answer, and nothing else happens fn add(x: i32, y: i32) -> i32 { x + y } // Side effect: it also prints, and the signature does not tell you that fn add_and_print(x: i32, y: i32) -> i32 { let result = x + y; println!("{} + {} = {}", x, y, result); result } }
A pure function is easier to test and easier to trust, because nothing outside it changes.
Both are fine. Just know which one you are writing.
Passing parameters
Here's where we get a preview of the memory stuff we'll really digest later.
3 ways to pass parameters
- Copying a parameter (default for
i32,bool,f64, other basic types) - Take ownership of a parameter (so it can change) (default for
String, other complex types) - Borrowing a parameter (to "peek" at 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 - Basic types like
i32,f64,boolare automatically copied - no worries there - Use
&before the parameter type when you don't need to modify it - If Rust complains about ownership, try following its suggestion or adding
& - You typically can't use a reference (
&) in a return value - that's why you'll seeStringas a return type more often than&str
Examples:
fn print_name(name: &str) { /* name is borrowed - original still usable */ }
fn calculate_area(width: f64, height: f64) -> f64 { /* both copied */ }
Just enough if to get through the activity
We've glossed over this so far. Here is the shape of it, and we do branching properly next lecture.
Syntax:
if condition {
//
} else if other_condition {
//
} else {
//
}
else ifandelseparts optional
Bringing it together with expressions
You can even use conditional expressions as values!
Python:
z = 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 fn main(){ let x = 4; println!("{}",if x == 7 {100} else {1.2}); }
Activity time!
We'll have our first hand-coding practice session!
You can work next to someone but write out your own sheet.
If you worked with someone, swap with someone else for feedback.
We'll go over answers at the end or start of next class.