Functions and Scopes

Functions are a mainstay of structured programming. C and C++ both support them:

#include <stdio.h>

void print() {
    printf("Hello, World");
}

int main() {
    print();
}

Does exactly what you expect: it prints "Hello, World!". The equivalent Rust is similar:

fn print() {
    println!("Hello, World!");
}

fn main() {
    print();
}

Returning Data from Functions

#include <stdio.h>

int add_one(int i) {
    return i+1;
}

int main() {
    int x = add_one(5);
    printf("%d", x);
}

Here you are declaring a function named add_one, with the return type int. You accept a parameter named i, and return i+1.

The Rust syntax is quite different:

fn add_one(i: i32) -> i32 {
    i + 1
}

fn main() {
    let x = add_one(5);
    println!("{x}");
}

The syntax differences are quite obvious:

  • The return type goes on the end, prefixed with ->.
  • Parameters are declared "name: type", rather than "type name".
  • There's no return statement! By default, Rust functions always return the result of the last expression. In idiomatic Rust, you'll usually see functions declared in this way.

If you miss return, it's still there:

fn add_one(i: i32) -> i32 {
    return i + 1;
}

fn main() {
    let x = add_one(5);
    println!("{x}");
}

Notice that to use return you need to add a semicolon - but the first version didn't have one! Lines with a semicolon are still expressions---but they return the "unit type" (()). So you can either omit the semicolon to have the expression "fall out" of the function, or you can use return and a semicolon. That's a little confusing, so let's look at some underlying Rust.

In Rust, everything returns.

fn foo() {}

fn main() {
    let i = foo();
    println!("{i:?}");
}

Notice we've used :?, the debug print again.

The program prints (). That's because () is like void - but it has a value (admittedly not a very useful one). So if you assign the result of a statement that ends in a ;, you are setting it to the unit type---which is probably not what you wanted.

Rust also supports expression assignment:

fn main() {
    let i = 5;
    let i = if i < 5 {
            1
        } else {
            0
        };
    println!("{i}");
}

Rust doesn't have a ternary operator!

You can assign from an expression or conditional just by returning using the no-semicolon syntax. This works for scopes, too:

fn main() {
    let i = {
        let mut accumulator = 0;
        for i in 0..10 {
            accumulator += i;
        }
        accumulator
    };
    println!("{i}");
}

Note that you can't use the return keyword when you do this---return explicitly returns out of the current function.

How about if I want to return multiple potential values from a function?

You can either make sure that every branch implicitly returns:

fn test(i: i32) -> i32 {
    if i < 10 {
        0
    } else {
        1
    }
}

fn main() {
    println!("{}", test(5));
}

Or you can use early return:

fn test(i: i32) -> i32 {
    if i < 10 {
        return;
    }
    1
}

fn main() {
    println!("{}", test(5));
}