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 + 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

#![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

  1. A parameter can be copied into a function (default for i32, bool, f64, other basic types)
  2. A function can take ownership of a parameter (default for String, other complex types)
  3. 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 &str for 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, bool are 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 see String as 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

  1. Can you change x to a different type using mut? Using shadowing?
  2. What's the largest value a u8 can hold?
  3. 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;
}
  1. 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 if and else parts 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

  1. Use consistent indentation (4 spaces or tabs)
  2. Keep conditions readable - use parentheses for clarity when needed
  3. Prefer early returns in functions to reduce nesting
  4. Use else if for multiple conditions rather than nested if

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);
}