Codecademy Logo

Functions

Print Cheatsheet

Declaring Functions

We can declare a function in Rust with the fn keyword. Declaring a function requires supplying a name for the function, any potential parameters, and a block for the body of the function.

// Declaring a function
fn say_howdy() {
println!("Howdy!");
}
// Calling a function
say_howdy();

Return Values

We can specify a return value for a function with the -> operator followed by the returned type. This is placed after the function name and before the function’s block.

// Here we are returning a `i32`.
fn another_function() -> i32 {
27
}
// Assigning a returned value to a variable.
let integer = another_function();
println!("{integer}"); // This will print "27".

Parameters

Our functions can take data as input to operate on. These are called input parameters and in Rust, parameters always require a type signature.

Type signatures are declared with the parameter name, followed by a :, followed by the type. Multiple parameters are separated by a ,.

// This multiply() function takes two arguments of type u32 and multiplies them together
fn multiply(first: u32, second: u32) -> u32 {
first * second
}

Closure Syntax

Closures follow a very similar syntax to functions but with input parameters placed between ||. Here we have declared a function that squares an integer and a closure that accomplishes the same task:

// Function
fn square_function(a: i32) -> i32 { a * a }
// Closure
let square_closure = |a: i32| -> i32 { a * a };

We rarely see closures in this verbose form thanks to Rust’s ability to infer parameter and return types. Additionally, the body of a closure does not require {} braces.

When we store a closure as a variable, we can call it the same we would a function.

// Single argument
let square = |a| a * a;
// Multiple arguments are separated by a comma
let multiply = |a, b| a * b;
// No arguments
let hundred = || 10 * 10;
// Calling closures that were saved as variables
square(5);
multiply(5, 10);
hundred();

Iteration

Any type that implements the Iterator trait gives us access to a plethora of methods that allow us to operate on collections without having to use a for loop.

We can create an iterator with the iter() method and then proceed through the collection with next().

let numbers = [1,2,3];
let mut numbers = numbers.iter();
if let Some(first) = numbers.next() {
println!("{first}");
}
if let Some(second) = numbers.next() {
println!("{second}");
}

collect() Method

The collect() method will transform an iterator back into a collection. Type annotations for the returned type are required if they cannot be inferred.

let mut numbers = [10, 20, 30].iter();
numbers.next();
numbers.next();
let remaining_numbers: Vec<&u32> = numbers.collect(); // [30]
// Turbo-fish type annotation
let remaining_numbers = numbers.collect::<Vec<&u32>>();
// Since iterators are consuming, remaining_numbers is a Vec with a single value of 30.

enumerate() Method

We can access the indices of a collection while iterating with the enumerate() method.

let names = ["Li", "Patrick", "Omar"];
let enumerated: Vec<(usize, &&str)> = names.iter()
.enumerate()
.filter( |(i, n)| i > &1)
.collect();
println!("{enumerated:?}"); // [(2, "Omar")]

filter() Method

The filter() method will only return values that satisfy a provided boolean conditional. Here we are returning only uppercase characters.

let chars = ['a', '1', 'E', 'F'];
let filtered: Vec<&char> = chars.iter()
.filter(|c| c.is_uppercase())
.collect();
println!("{filtered:?}"); // ['E', 'F']

map() Method

The map() method takes a closure that will operate on each value of the collection. Here we are doubling each item in our numbers array.

let numbers = [1, 2, 3];
let nums: Vec<i32> = numbers.iter()
.map(|x| x * 2 )
.collect();
println!("{nums:?}"); // [2, 4, 6]