Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lecture 7 - Functions: parameters, returns, and expressions

Two arrows entering a black box and one leaving it

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 + 2 is an expression
  • let 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

  1. Copying a parameter (default for i32, bool, f64, other basic types)
  2. Take ownership of a parameter (so it can change) (default for String, other complex types)
  3. 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 &str for string parameters
  • Basic types like i32, f64, bool are 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 see String as 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 if and else parts 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.