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 functionfn say_howdy() {println!("Howdy!");}// Calling a functionsay_howdy();
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".
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 togetherfn multiply(first: u32, second: u32) -> u32 {first * second}
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()
MethodThe 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 annotationlet remaining_numbers = numbers.collect::<Vec<&u32>>();// Since iterators are consuming, remaining_numbers is a Vec with a single value of 30.
enumerate()
MethodWe 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()
MethodThe 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()
MethodThe 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]
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:
// Functionfn square_function(a: i32) -> i32 { a * a }// Closurelet 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 argumentlet square = |a| a * a;// Multiple arguments are separated by a commalet multiply = |a, b| a * b;// No argumentslet hundred = || 10 * 10;// Calling closures that were saved as variablessquare(5);multiply(5, 10);hundred();