Chapter 8: Functions and Methods
In Rust, as in C and many other procedural or functional languages, functions are the primary tool for organizing code into named, reusable blocks. They allow you to group a sequence of statements and expressions to perform a specific task. Functions can accept input values, known as parameters, process them, and optionally produce an output value, known as a return value. This practice of breaking down programs into smaller, well-defined units is crucial for improving code readability, testability, and maintainability. Rust utilizes functions in two main ways: as standalone functions for general operations, and as methods, which are functions defined within the context of a struct, enum, or trait, typically acting upon instances of that type.
Standalone functions in Rust are also versatile: you can store them in variables, pass them as arguments to other functions, and return them as results, much like any other data type.
Rust also features anonymous functions, known as closures, which can capture variables from their surrounding environment. Closures are powerful tools and will be covered in detail in Chapter 12.
This chapter explores the core concepts of defining, calling, and utilizing both standalone functions and methods in Rust. We will cover:
- The role and structure of the
mainfunction. - Basic function definition syntax and calling conventions.
- Function parameters, including different ways to pass data (value, reference, mutable reference) and how they relate to ownership and borrowing.
- Return types and mechanisms for returning values (explicit
returnvs. implicit expression). - Function scope rules, including nested functions.
- How Rust handles the absence of default parameters and named arguments, and common patterns to achieve similar results.
- Using slices and tuples effectively as function parameters and return types.
- Introduction to generic functions for writing type-agnostic code.
- Function pointers and their use in higher-order functions.
- Recursion and the status of tail call optimization (TCO) in Rust.
- Function inlining as a performance optimization.
- Method syntax for functions associated with specific types (
structs,enums). - Associated functions (static methods) versus instance methods.
- Rust’s approach instead of traditional function overloading.
- Type inference limitations regarding function return types, and the
impl Traitsyntax. - Alternatives to C-style variadic functions using Rust macros.
8.1 The main Function: The Program’s Entry Point
Every executable Rust program must contain exactly one function named main. This function serves as the starting point when the compiled binary is executed.
fn main() {
println!("Hello from the main function!");
}
Key characteristics of main:
- Parameters: By default,
maintakes no parameters. To access command-line arguments passed to the program, you use thestd::env::args()function, which returns an iterator over the arguments. - Return Type: The
mainfunction typically returns the unit type(), signifying no specific value is returned (similar tovoidin C functions that don’t return a value). Alternatively,maincan return aResult<(), E>whereEis some error type that implementsstd::process::Termination. This is particularly useful for propagating errors encountered during program execution, often used in conjunction with the?operator for concise error handling.
8.1.1 Accessing Command-Line Arguments
You can collect command-line arguments into a Vec<String> using std::env::args():
use std::env;
fn main() {
// The first argument (args[0]) is typically the path to the executable itself.
let args: Vec<String> = env::args().collect();
println!("Program path: {}", args.get(0).unwrap_or(&"Unknown".to_string()));
println!("Arguments passed: {:?}", &args[1..]);
// Example: Check for a specific argument
if args.len() > 1 && args[1] == "--help" {
println!("Displaying help information...");
// ... logic to display help ...
}
}
8.1.2 Returning a Result from main
Returning Result from main provides a standard way to indicate whether the program executed successfully (Ok(())) or encountered an error (Err(E)). If main returns an Err, Rust will typically print the error description to standard error and exit with a non-zero status code.
use std::fs::File;
use std::io;
// main returns a Result to indicate success or failure (specifically I/O errors).
fn main() -> Result<(), io::Error> {
// Attempt to open a file that might not exist.
let _f = File::open("non_existent_file.txt")?;
// The '?' operator propagates the error if File::open fails.
println!("File opened successfully (this won't print if the file doesn't exist).");
// If everything succeeded, return Ok.
Ok(())
}
This pattern simplifies error handling at the top level of your application.
8.2 Defining and Calling Functions
Rust uses the fn keyword to define functions. A key difference from C/C++ is that Rust does not require forward declarations. You can define a function anywhere in the module (usually a .rs file), and call it from code that appears earlier in the same file. The compiler processes the entire module before resolving calls.
8.2.1 Basic Function Definition Syntax
The general syntax for defining a function is:
fn function_name(parameter1: Type1, parameter2: Type2) -> ReturnType {
// Function body: statements and expressions
// The last expression can be the return value (if no semicolon)
}
fn: Keyword to start a function definition.function_name: The identifier for the function (snake_case is conventional).(): Parentheses enclosing the parameter list. These are required even if the function takes no parameters.parameter: Type: Inside the parentheses, each parameter consists of a name followed by a colon and its type. Parameters are separated by commas.-> ReturnType: An optional arrow->followed by the type of the value the function returns. If omitted, the function returns the unit type().{ ... }: The function body – a block, enclosed in curly braces.
Example:
fn main() {
// Calling greet before its definition is allowed.
greet("World");
let sum = add_numbers(5, 3);
println!("5 + 3 = {}", sum);
}
// A function that takes a string slice and prints a greeting. Returns ().
fn greet(name: &str) {
println!("Hello, {}!", name);
}
// A function that takes two i32 integers and returns their sum.
fn add_numbers(a: i32, b: i32) -> i32 {
a + b // This expression is the return value
}
Comparison with C:
In C, if you call add_numbers before its definition, you typically need a forward declaration (prototype) like int add_numbers(int a, int b); near the top of the file or in a header file. Rust eliminates this requirement within a module.
8.2.2 Calling Functions
To call a function, use its name followed by parentheses (). If the function expects arguments, provide them inside the parentheses in the correct order and with matching types.
fn print_coordinates(x: i32, y: i32) {
println!("Coordinates: ({}, {})", x, y);
}
// A function that takes no arguments
fn display_separator() {
println!("--------------------");
}
fn main() {
print_coordinates(10, 20); // Call with arguments 10 and 20.
display_separator(); // no arguments - parentheses are still required.
}
- Parentheses
(): Always required for a function call, even if the function takes no parameters, as seen withdisplay_separator(). - Arguments: If the function defines parameters, you must provide arguments inside the parentheses. These arguments must match the number, type, and order of the parameters defined in the function signature. Multiple arguments are separated by commas (
,), as seen withprint_coordinates(10, 20).
8.2.3 Ignoring Function Return Values
If a function returns a value but you don’t need it, you can simply call the function without assigning the result to a variable.
fn get_status_code() -> u16 {
200 // Represents an HTTP OK status
}
fn main() {
get_status_code(); // The returned value 200 is discarded.
}
However, some functions, particularly those returning Result<T, E>, are often marked with the #[must_use] attribute. If you ignore the return value of such a function, the Rust compiler will issue a warning, as ignoring it might mean overlooking a potential error or important outcome.
#[must_use = "this Result must be handled"]
fn check_condition() -> Result<(), String> {
// ... logic that might fail ...
Ok(())
}
fn main() {
check_condition(); // Compiler warning: unused result which must be used
// To explicitly ignore a #[must_use] value:
let _ = check_condition(); // Assigning to '_' silences the warning.
// or simply:
// _ = check_condition();
}
It’s generally good practice to handle or explicitly ignore Result values rather than letting them be implicitly discarded.
8.3 Function Parameters and Data Passing
Rust functions can accept parameters in various forms, each affecting ownership, mutability, and borrowing. Within a function’s body, parameters behave like ordinary variables. This section describes the fundamental parameter types, when to use them, and how they compare to C function parameters.
We will illustrate parameter passing with the String type, which is moved into the function when passed by value and can no longer be used at the call site. Note that primitive types implementing the Copy trait will be copied by value instead of moved.
8.3.1 Passing by Value (T)
When a parameter has type T (and T does not implement Copy), the value is moved into the function. The function takes ownership, and the original variable in the caller’s scope becomes inaccessible.
// This function takes ownership of the String.
fn process_string(s: String) {
println!("Processing owned string: {}", s);
// 's' goes out of scope here, and the memory is deallocated.
}
fn main() {
let message = String::from("Owned data");
process_string(message); // Ownership of 'message' is transferred to process_string.
// Trying to use 'message' here would cause a compile-time error:
// println!("Original message: {}", message); // Error: value borrowed after move
}
- Use Cases: Primarily when the function needs to consume the value (e.g., send it elsewhere, store it permanently) or take final ownership (ensuring the value is dropped or managed exclusively by the function). This pattern guarantees that the original variable cannot be used after the call. It’s also used when the function manages the lifecycle of a resource represented by
T. While a functionfn transform(value: T) -> Ucan exist, ifvalueisn’t modified in place (which it can’t be ifTisn’tmut), often taking&Tmight be more flexible if the original isn’t meant to be consumed. - Comparison to C: Similar to passing a struct by value, but Rust’s borrow checker prevents using the original variable after the move.
8.3.2 Passing by Mutable Value (mut T)
You can declare a value parameter as mutable using mut T. Ownership is still transferred (for non-Copy types), but the function is allowed to modify the value it now owns.
// This function takes ownership and can modify the owned value.
fn modify_string(mut s: String) { // 'mut s' allows modification inside the function
s.push_str(" (modified)");
println!("Modified owned string: {}", s);
// s is dropped here unless returned
}
// Example of modifying and returning ownership
fn modify_and_return(mut s: String) -> String {
s.push_str(" and returned");
s // Return ownership of the modified string
}
fn main() {
// NOTE: 'message' does NOT need to be 'mut' here!
let message = String::from("Mutable owned data");
// modify_string takes ownership, message cannot be used after
modify_string(message);
// println!("{}", message); // Error: use of moved value
let message2 = String::from("Another message");
// modify_and_return takes ownership, but returns it
let modified_message2 = modify_and_return(message2);
// println!("{}", message2); // Error: use of moved value 'message2'
println!("{}", modified_message2); // Ok: "Another message and returned"
}
Note on Caller Variable Mutability: Notice in the examples that
messageandmessage2were declared usinglet, notlet mut. When passing by value (mut T), the function takes full ownership via a move. Themutin the function signature (e.g.,mut s: String) only grants the function permission to mutate the value it now exclusively owns. Since the caller loses ownership and cannot access the original variable after the move, whether the original variable was declaredmutis irrelevant.This contrasts sharply with passing a mutable reference (
&mut T), where the caller retains ownership and merely lends out mutable access. To grant this mutable borrow permission, the caller’s variable must be declared withlet mut.
- Use Cases: When the function needs to take ownership and modify the value it now owns. This could be for internal computations, using the value as a mutable scratch space, or for patterns like functional builders/chaining. In such patterns, a configuration object or state might be passed through several functions, each taking ownership via
mut T, modifying it in place, and then returning ownership (fn step(mut config: Config) -> Config). This can be efficient as it may avoid allocations needed if new instances were created at each step. However, for simply modifying the caller’s original data without transferring ownership back and forth,&mut Tremains the more common choice. - Comparison to C: Similar to passing a struct by value regarding locality (changes don’t affect the caller), but distinct due to Rust’s move semantics. Modifications inside the function apply only to the specific instance whose ownership was transferred into the function via the move.
8.3.3 Passing by Shared Reference (&T)
To allow a function to read data without taking ownership, pass a shared reference (&T). This is known as borrowing. The caller retains ownership, and the data must remain valid while the reference exists.
// This function borrows the String immutably.
fn calculate_length(s: &String) -> usize {
s.len() // Can read from 's', but cannot modify it.
}
fn main() {
let message = String::from("Immutable borrow");
let length = calculate_length(&message); // Pass a reference to 'message'.
println!("The length of '{}' is {}", message, length);
// 'message' is still valid and owned here.
}
- Use Cases: Very common when a function only needs read-access to data. Avoids costly cloning or ownership transfer.
- Comparison to C: Similar to passing a pointer to
constdata (e.g.,const char*orconst MyStruct*). Rust guarantees at compile time that the referenced data cannot be mutated through this reference and that the data outlives the reference.
8.3.4 Passing by Mutable Reference (&mut T)
To allow a function to modify data owned by the caller, pass a mutable reference (&mut T). This is also borrowing, but exclusively – while the mutable reference exists, no other references (mutable or shared) to the data are allowed.
// This function borrows the String mutably.
fn append_greeting(s: &mut String) {
s.push_str(", World!"); // Can modify the borrowed String.
}
fn main() {
// 'message' must be declared 'mut' to allow mutable borrowing.
let mut message = String::from("Hello");
append_greeting(&mut message); // Pass a mutable reference.
println!("Modified message: {}", message);
// Output: Modified message: Hello, World!
// 'message' is still owned here, but its content has been changed.
}
- Use Cases: Very common when a function needs to modify data in place without taking ownership (e.g., modifying elements in a vector, updating fields in a struct).
- Comparison to C: Similar to passing a non-
constpointer (e.g.,char*orMyStruct*) to allow modification. Rust’s borrow checker provides stronger safety guarantees by preventing simultaneous mutable access or mixing mutable and shared access, eliminating data races at compile time.
8.3.5 Summary Table: Choosing Parameter Types
| Parameter Type | Ownership | Modification of Original | Caller Variable mut Required? | Typical Use Case | C Analogy (Approximate) |
|---|---|---|---|---|---|
T (non-Copy) | Transferred | No | No | Consuming data, final ownership transfer | Pass struct by value |
T (Copy type) | Copied | No | No | Passing small, cheap-to-copy data | Pass primitive by value |
mut T (non-Copy) | Transferred | No (Local owned value) | No | Modifying owned value before consumption/return | Pass struct by value |
&T | Borrowed | No | No | Read-only access, avoiding copies | const T* |
&mut T | Borrowed | Yes | Yes | Modifying caller’s data in-place | T* (non-const) |
Note on Shadowing Parameters: You can declare a new local variable with the same name as an immutable parameter, making it mutable within the function’s scope. This is called shadowing.
fn process_value(value: i32) {
// 'value' parameter is immutable.
// Shadow 'value' with a new mutable variable.
let mut value = value;
value += 10;
println!("Processed value: {}", value);
}
fn main() {
process_value(5); // Prints: Processed value: 15
}
Side Note on mut with Reference Parameters:
In Rust, you might occasionally encounter function signatures like fn func(mut param: &T) or fn func(mut param: &mut T). Adding mut directly before the parameter name (mut param) makes the binding param mutable within the function’s scope. This means you could reassign param to point to a different value of type &T or &mut T respectively.
- For
mut param: &T, this does not allow modifying the data originally pointed to byparam, because the type&Trepresents a shared, immutable borrow. - For
mut param: &mut T, the underlying data can be modified because the type&mut Tis a mutable borrow, regardless of whether the bindingparamitself ismut.
This pattern of making the reference binding itself mutable is relatively uncommon in idiomatic Rust compared to simply passing &T or &mut T.
8.4 Returning Values from Functions
Functions in Rust are capable of returning values, and their return type is explicitly defined in the function signature after the -> arrow. If no return type is specified, the function implicitly returns the unit type (), which is an empty tuple.
8.4.1 Syntax for Returning Values
// Returns an i32 value.
fn give_number() -> i32 {
42 // Implicit return of the expression's value
}
// Returns a new String.
fn create_greeting(name: &str) -> String {
let mut greeting = String::from("Hello, ");
greeting.push_str(name);
greeting // Implicit return of the variable 'greeting'
}
fn main() {
let number = give_number();
let text = create_greeting("Alice");
println!("Number: {}", number);
println!("Greeting: {}", text);
}
8.4.2 Explicit return vs. Implicit Return
Rust provides two ways to specify the return value of a function:
-
Implicit Return: If the last statement in a function body is an expression (without a trailing semicolon), its value is automatically returned. This is the idiomatic style in Rust for the common case.
#![allow(unused)] fn main() { fn multiply(a: i32, b: i32) -> i32 { a * b // No semicolon, this expression's value is returned. } } -
Explicit
returnKeyword: You can use thereturnkeyword to exit the function immediately with a specific value. This is often used for early returns, such as in error conditions or conditional logic.#![allow(unused)] fn main() { fn find_first_even(numbers: &[i32]) -> Option<i32> { for &num in numbers { if num % 2 == 0 { return Some(num); // Early return if an even number is found. } } None // Implicit return if the loop finishes without finding an even number. } }
A function’s body is fundamentally a block expression. As discussed, blocks that either are empty or conclude with a statement evaluate to the unit type ().
Therefore, if a function signature specifies a return type (e.g., -> i32), but the function’s body ends with a statement (such as an expression terminated by a semicolon, e.g., a * b;), a type mismatch error will result. This is because the function implicitly returns () instead of the expected i32. To successfully return a value, the final construct in the function body must be an expression not terminated by a semicolon.
#![allow(unused)]
fn main() {
fn multiply_buggy(a: i32, b: i32) -> i32 {
a * b; // Semicolon makes this a statement, function returns () implicitly.
// Compile Error: expected i32, found ()
}
}
Comparison with C: In C, you must use the return value; statement to return a value from a function. Functions declared void either have no return statement or use return; without a value. Rust’s implicit return from the final expression is a convenient shorthand not found in C.
8.4.3 Returning References (and Lifetimes)
Functions can return references (&T or &mut T), but this requires careful consideration of lifetimes. A returned reference must point to data that will remain valid after the function call has finished.
Typically, this means the returned reference must point to:
- Data that was passed into the function via a reference parameter.
- Data that exists outside the function (e.g., a
staticvariable).
You cannot return a reference to a variable created locally inside the function, because that variable will be destroyed when the function exits, leaving the reference dangling (pointing to invalid memory). The Rust compiler prevents this with lifetime checks.
// This function takes a slice and returns a reference to its first element.
// The lifetime 'a ensures the returned ref. is valid as long as the input slice is.
fn get_first<'a>(slice: &'a [i32]) -> &'a i32 {
&slice[0] // Returns a reference derived from the input slice.
}
// This function attempts to return a reference to a local variable (Compiler Error).
// fn get_dangling_reference() -> &i32 {
// let local_value = 10;
// &local_value // Error: `local_value` does not live long enough
// }
fn main() {
let numbers = [10, 20, 30];
let first = get_first(&numbers); // 'first' borrows from 'numbers'.
println!("The first number is: {}", first);
// 'first' remains valid as long as 'numbers' is in scope.
// let dangling = get_dangling_reference(); // This would not compile.
}
Returning mutable references (&mut T) follows the same lifetime rules. This ability to safely return references, especially mutable ones, is a powerful feature enabled by Rust’s borrow checker, preventing common C/C++ errors like returning pointers to stack variables. Lifetimes are covered more deeply in a later chapter.
8.5 Function Scope and Nested Functions
Rust supports defining functions both at the top level of a module (similar to C) and nested within other functions.
8.5.1 Scope of Top-Level Functions
Functions defined directly within a module (not inside another function or block) are called top-level functions. They are visible throughout the entire module in which they are defined, regardless of the order of definition.
To make a top-level function accessible from other modules, you must mark it with the pub keyword (for public).
mod utils {
// This function is private to the 'utils' module by default.
fn helper() {
println!("Private helper function.");
}
// This function is public and can be called from outside 'utils'.
pub fn perform_task() {
println!("Performing public task...");
helper(); // Can call private functions within the same module.
}
}
fn main() {
utils::perform_task(); // OK: perform_task is public.
// utils::helper(); // Error: helper is private.
}
8.5.2 Nested Functions
Rust allows defining functions inside the body of other functions. These are called nested functions or inner functions. A nested function is only visible and callable within the scope of the outer function where it is defined.
fn outer_function(x: i32) {
println!("Entering outer function with x = {}", x);
// Define a nested function.
fn inner_function(y: i32) {
println!(" Inner function called with y = {}", y);
// Cannot access 'x' from outer_function here.
// println!(" Cannot access x: {}", x); // Compile Error!
}
// Call the nested function.
inner_function(x * 2);
println!("Exiting outer function.");
}
fn main() {
outer_function(5);
// inner_function(10); // Error: inner_function is not in scope here.
}
Key difference from Closures: Nested functions in Rust cannot capture variables from their enclosing environment (like x in the example above). If you need a function-like construct that can access variables from its surrounding scope, you should use a closure (Chapter 12). Nested functions are simpler entities, essentially just namespaced helper functions local to another function’s implementation.
8.6 Handling Optional and Named Parameters
Unlike languages such as Python or C++, Rust does not have built-in support for:
- Default parameter values: Providing a default value if an argument isn’t supplied.
- Named arguments: Passing arguments using
parameter_name = valuesyntax, allowing arbitrary order.
All function arguments in Rust must be explicitly provided by the caller in the exact order specified in the function signature.
However, Rust offers idiomatic patterns to achieve similar flexibility:
8.6.1 Using Option<T> for Optional Parameters
The standard library type Option<T> (Chapter 14) can represent a value that might be present (Some(value)) or absent (None). This is commonly used to simulate optional parameters.
// 'level' is an optional parameter.
fn log_message(message: &str, level: Option<&str>) {
// Use unwrap_or to provide a default value if 'level' is None.
let log_level = level.unwrap_or("INFO");
println!("[{}] {}", log_level, message);
}
fn main() {
log_message("User logged in.", None); // Use default level "INFO".
log_message("Disk space low!", Some("WARN")); // Provide a specific level.
}
8.6.2 The Builder Pattern for Complex Configuration
For functions with multiple configurable parameters, especially optional ones, the Builder Pattern is often used. This involves creating a separate Builder struct that accumulates configuration settings via method calls before finally constructing the desired object or performing the action.
struct WindowConfig {
title: String,
width: u32,
height: u32,
resizable: bool,
}
// Builder struct
struct WindowBuilder {
title: String,
width: Option<u32>,
height: Option<u32>,
resizable: Option<bool>,
}
impl WindowBuilder {
// Start building with a mandatory parameter (title)
fn new(title: String) -> Self {
WindowBuilder {
title,
width: None,
height: None,
resizable: None,
}
}
// Methods to set optional parameters
fn width(mut self, width: u32) -> Self {
self.width = Some(width);
self // Return self to allow chaining
}
fn height(mut self, height: u32) -> Self {
self.height = Some(height);
self
}
fn resizable(mut self, resizable: bool) -> Self {
self.resizable = Some(resizable);
self
}
// Final build method using defaults for unspecified options
fn build(self) -> WindowConfig {
WindowConfig {
title: self.title,
width: self.width.unwrap_or(800), // Default width
height: self.height.unwrap_or(600), // Default height
resizable: self.resizable.unwrap_or(true), // Default resizable
}
}
}
fn main() {
let window1 = WindowBuilder::new("My App".to_string()).build(); // Use all defaults
let window2 = WindowBuilder::new("Editor".to_string())
.width(1024)
.height(768)
.resizable(false)
.build(); // Specify some options
println!("Window 1: width={}, height={}, resizable={}",
window1.width, window1.height, window1.resizable);
println!("Window 2: width={}, height={}, resizable={}",
window2.width, window2.height, window2.resizable);
}
The Builder pattern provides clear, readable configuration and handles defaults gracefully, making it a robust alternative to named/default parameters for complex function calls or object construction.
8.7 Using Slices and Tuples with Functions
Slices and tuples are common data structures in Rust, frequently used as function parameters and return types. String slices were already introduced as useful function parameter types in Section 6.5.5.
8.7.1 Slices (&[T] and &str)
Slices provide a view into a contiguous sequence of elements, representing all or part of data structures like arrays, Vec<T>s, or Strings, without taking ownership. Passing slices is efficient as it only involves passing a pointer and a length.
String Slices (&str): Used for passing views of string data.
// Takes a string slice and returns the first word (also as a slice).
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i]; // Return slice up to the space
}
}
&s[..] // Return the whole string slice if no space is found
}
fn main() {
let sentence = String::from("Hello beautiful world");
let word = first_word(&sentence); // Pass reference to the String
println!("The first word is: {}", word); // Output: The first word is: Hello
let literal = "Another example";
let word2 = first_word(literal); // Works directly with string literals (&str)
println!("The first word is: {}", word2); // Output: The first word is: Another
}
Array/Vector Slices (&[T]): Used for passing views of arrays or vectors containing elements of type T.
// Calculates the sum of elements in an i32 slice.
fn sum_slice(slice: &[i32]) -> i32 {
let mut total = 0;
for &item in slice { // Iterate over the elements in the slice
total += item;
}
total
}
fn main() {
let numbers_array = [1, 2, 3, 4, 5];
let numbers_vec = vec![10, 20, 30];
println!("Sum of array: {}", sum_slice(&numbers_array[..]));
println!("Sum of part of vec: {}", sum_slice(&numbers_vec[1..]));
}
Remember that when returning slices, lifetimes must ensure the reference remains valid (as discussed in Section 8.4.3).
As noted in Section 6.5.6, mutable slice parameters (&mut [T]) are also permitted. Functions can modify the contents of the slice, but not its length. For string slices (&mut str), an additional constraint is that all allowed modifications must preserve valid UTF-8 encoding.
8.7.2 Tuples
Tuples are fixed-size collections of values of potentially different types. They are useful for grouping related data, especially for returning multiple values from a function.
Tuples as Parameters:
// Represents a 2D point.
type Point = (i32, i32);
fn display_point(p: Point) {
println!("Point coordinates: ({}, {})", p.0, p.1); // Access elements by index
}
fn main() {
let my_point = (10, -5);
display_point(my_point);
}
Tuples as Return Types: Commonly used to return multiple results without defining a dedicated struct.
// Calculates sum and product, returning them as a tuple.
fn calculate_stats(a: i32, b: i32) -> (i32, i32) {
(a + b, a * b) // Return a tuple containing sum and product
}
fn main() {
let num1 = 5;
let num2 = 8;
let (sum_result, product_result) = calculate_stats(num1, num2);
// Destructure the returned tuple
println!("Numbers: {}, {}", num1, num2);
println!("Sum: {}", sum_result);
println!("Product: {}", product_result);
}
8.8 Generic Functions
Generics allow writing functions that can operate on values of multiple different types, while still maintaining type safety. This avoids source code duplication. Generic functions declare type parameters (typically denoted by T, U, etc.) enclosed in angle brackets (<>) after the function name. These type parameters then act as placeholders for concrete types within the function’s signature (for parameters and return types) and body. Often, these type parameters require specific capabilities, expressed using trait bounds.
Generics are a large topic, covered more extensively in Chapter 11, but here’s an introduction.
Example: A Generic max function
Without generics, you’d need separate functions for i32, f64, etc.
#![allow(unused)]
fn main() {
fn max_i32(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
fn max_f64(a: f64, b: f64) -> f64 {
if a > b { a } else { b }
}
// ... potentially more versions
}
With generics, you write one function:
use std::cmp::PartialOrd; // Trait required for comparison operators like >
// T is a type parameter.
// T: PartialOrd is a trait bound, meaning T must implement PartialOrd.
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
if a > b {
a
} else {
b
}
}
fn main() {
println!("Max of 5 and 10: {}", max_generic(5, 10)); // Works with i32
println!("Max of 3.14 and 2.71: {}", max_generic(3.14, 2.71)); // Works with f64
println!("Max of 'a' and 'z': {}", max_generic('a', 'z')); // Works with char
}
<T: PartialOrd>: Declares a generic typeTthat must implement thePartialOrdtrait (which provides comparison methods like>and<).- The function signature uses
Twherever a concrete type (likei32) would have been used.
The compiler generates specialized versions of the generic function for each concrete type used at compile time (e.g., one version for i32, one for f64). This process is called monomorphization, ensuring generic code runs just as efficiently as specialized code, without runtime overhead.
8.9 Function Pointers and Higher-Order Functions
In Rust, functions are first-class citizens. This means they can be treated like other values: assigned to variables, passed as arguments to other functions, and returned from functions.
8.9.1 Function Pointers
A variable or parameter can hold a function pointer, which references a specific function. The type of a function pointer is denoted by fn followed by the parameter types and return type. For example, fn(i32, i32) -> i32 is the type of a pointer to a function that takes two i32s and returns an i32.
type Binop = fn(i32, i32) -> i32;
fn add(a: i32, b: i32) -> i32 { a + b }
fn subtract(a: i32, b: i32) -> i32 { a - b }
fn multiply(a: i32, b: i32) -> i32 { a * b }
// This function takes a function pointer as an argument.
fn apply_operation(operation: Binop, x: i32, y: i32) -> i32 {
operation(x, y) // Call the function via the pointer
}
fn main() {
let mut operation_to_perform: Binop; // Use type alias
operation_to_perform = add; // Assign 'add' function to the pointer variable
println!("Result of add: {}", apply_operation(operation_to_perform, 10, 5));
operation_to_perform = subtract; // Reassign to 'subtract' function
println!("Result of subtract: {}", apply_operation(operation_to_perform, 10, 5));
// You can also pass the function name directly where a pointer is expected.
println!("Directly passing multiply: {}", apply_operation(multiply, 10, 5));
}
Note: When assigning functions to variables, as in let bo: Binop = add;, the & operator is not required on the function name.
Safety and Restrictions
- Despite the term function pointer, Rust’s function pointers are safe and type-checked. It is not possible to call invalid or uninitialized addresses, as can happen in C.
- Their capabilities are intentionally limited: they cannot be cast to arbitrary integers or used for unchecked jumps, unlike raw pointers in unsafe C code.
Function pointer types represent functions whose exact identity may not be known at compile time. Function pointers are useful for implementing callbacks, strategy patterns, or selecting behavior dynamically based on data. However, using function pointers can sometimes inhibit compiler optimizations like inlining compared to direct function calls or monomorphized generics.
8.9.2 Higher-Order Functions
A function that either takes another function as an argument or returns a function is called a higher-order function. apply_operation in the example above is a higher-order function because it takes operation (a function pointer) as an argument.
Functions can also return function pointers:
type Binop = fn(i32, i32) -> i32; // Using the type alias from before
fn get_HOF_operation(operator: char) -> Binop { // Return type is Binop
fn add(a: i32, b: i32) -> i32 { a + b }
fn subtract(a: i32, b: i32) -> i32 { a - b }
match operator {
'+' => add, // Return a pointer to the 'add' function
'-' => subtract, // Return a pointer to the 'subtract' function
_ => panic!("Unknown operator"),
}
}
fn main() {
let op = get_HOF_operation('+');
println!("Result (10 + 3): {}", op(10, 3)); // Call the returned function
let op2 = get_HOF_operation('-');
println!("Result (10 - 3): {}", op2(10, 3));
}
While function pointers are useful, closures (Chapter 12) are often more flexible in Rust because they can capture variables from their environment, whereas function pointers cannot. Higher-order functions frequently work with closures in idiomatic Rust code (e.g., methods like map, filter, fold on iterators).
8.10 Recursion and Tail Call Optimization
A function is recursive if it calls itself, either directly or indirectly. Recursion is a natural way to solve problems that can be broken down into smaller, self-similar subproblems.
8.10.1 Recursive Function Example: Factorial
The factorial function is a classic example: n! = n * (n-1)! with 0! = 1.
fn factorial(n: u64) -> u64 {
if n == 0 {
1 // Base case
} else {
n * factorial(n - 1) // Recursive step
}
}
fn main() {
println!("5! = {}", factorial(5)); // Output: 5! = 120
}
Each recursive call adds a new frame to the program’s call stack to store local variables, parameters, and the return address. If the recursion goes too deep (e.g., calculating factorial(100000)), it can exhaust the available stack space, leading to a stack overflow error and program crash. Recursive calls also typically incur some performance overhead compared to iterative solutions.
8.10.2 Tail Recursion and Tail Call Optimization (TCO)
A recursive call is in tail position if it is the very last action performed by the function before it returns. A function where all recursive calls are in tail position is called tail-recursive.
Example: Tail-Recursive Factorial We can rewrite factorial using an accumulator parameter to make the recursive call the last operation:
fn factorial_tailrec(n: u64, accumulator: u64) -> u64 {
if n == 0 {
accumulator // Base case: return the accumulated result
} else {
// The recursive call is the last thing done.
factorial_tailrec(n - 1, n * accumulator)
}
}
// Helper function to provide the initial accumulator value
fn factorial_optimized(n: u64) -> u64 {
factorial_tailrec(n, 1) // Start with accumulator = 1
}
fn main() {
println!("Optimized 5! = {}", factorial_optimized(5)); // Output: Optimized 5! = 120
}
Tail Call Optimization (TCO) is a compiler optimization where a tail call (especially a tail-recursive call) can be transformed into a simple jump, reusing the current stack frame instead of creating a new one. This effectively turns tail recursion into iteration, preventing stack overflow and improving performance.
Status of TCO in Rust: Critically, Rust does not currently guarantee Tail Call Optimization. While the underlying LLVM compiler backend can perform TCO in some specific situations (especially in release builds with optimizations enabled), it is not a guaranteed language feature you can rely on.
Implications: Deep recursion, even if written in a tail-recursive style, can still lead to stack overflows in Rust. For algorithms requiring deep recursion or unbounded recursion depth, you should prefer an iterative approach or simulate recursion using heap-allocated data structures (like a Vec acting as an explicit stack) if stack overflow is a concern.
8.11 Function Inlining
Inlining is a compiler optimization where the code of a called function is inserted directly at the call site, rather than performing an actual function call (which involves setting up a stack frame, jumping, and returning). Rust’s compiler (specifically, the LLVM backend) automatically performs inlining based on heuristics (function size, call frequency, optimization level, etc.) during release builds (cargo build --release).
Benefits of Inlining: Inlining primarily aims to reduce the overhead associated with function calls. More importantly, by making the function’s body visible within the caller’s context, it can unlock further optimizations:
- Constant Propagation: If arguments passed to the inlined function are compile-time constants, the compiler can often simplify the inlined code significantly.
- Dead Code Elimination: Conditional branches within the inlined function might become constant, allowing the compiler to remove unreachable code.
- Specialization: When generic functions or functions taking closures are inlined, the compiler can generate highly specialized code tailored to the specific types or closure being used, often resulting in performance equivalent to hand-written specialized code. (We will see more about closures and optimization in a later chapter).
You can influence inlining decisions using the #[inline] attribute:
#[inline]: Suggests to the compiler that inlining this function might be beneficial. It’s a hint, not a command.#[inline(always)]: A stronger hint, requesting the compiler to always inline the function if possible. The compiler might still decline if inlining is impossible or deemed harmful (e.g., for recursive functions without TCO, or if it leads to excessive code bloat).#[inline(never)]: Suggests the compiler should avoid inlining this function.
// Suggest inlining this small function.
#[inline]
fn add_one(x: i32) -> i32 {
x + 1
}
// Strongly request inlining.
#[inline(always)]
fn is_positive(x: i32) -> bool {
x > 0
}
// Discourage inlining (rarely needed).
#[inline(never)]
fn complex_calculation(data: &[u8]) {
// ... potentially large function body ...
println!("Performing complex calculation.");
}
fn main() {
let y = add_one(5); // May be inlined
let positive = is_positive(y); // Likely to be inlined
complex_calculation(&[1, 2, 3]); // Unlikely to be inlined
println!("y = {}, positive = {}", y, positive);
}
Trade-offs: While inlining reduces call overhead and enables optimizations, over-inlining (especially of large functions) can lead to code bloat, increasing the overall size of the compiled binary, which can negatively impact instruction cache performance. Relying on the compiler’s default heuristics is often sufficient, but #[inline] can be useful for performance-critical library code or very small, frequently called helper functions.
8.11.1 When Inlining Might Not Occur or Be Limited
While the compiler often performs inlining aggressively in optimized builds, certain technical and practical factors can prevent or limit it, even when hinted with #[inline] or #[inline(always)]:
- Optimization Level: Inlining is primarily an optimization feature of release builds (
--release,-C opt-level=3). Debug builds (-C opt-level=0) intentionally perform minimal inlining for faster compiles and better debugging. - Call Type:
- Indirect Calls: Calls via function pointers or dynamic dispatch (trait objects) generally cannot be inlined as the target function isn’t known at compile time.
- External/FFI Calls: Calls to external functions (e.g., C libraries) cannot be inlined as their body isn’t available to the Rust compiler.
- Recursion: Directly recursive functions usually cannot be fully inlined.
- Compilation Boundaries:
- Across Crates: Inlining code from dependency crates requires the function’s metadata (like MIR) to be available (common for generics or
#[inline]functions) or Link-Time Optimization (LTO) to be enabled. Without these conditions, cross-crate inlining of regular functions is limited. - Within Crates (CGUs): Incremental compilation divides crates into Code Generation Units (CGUs). Aggressive inlining across CGU boundaries might be restricted by default (unless LTO is on) to improve incremental build times. Inlining within a CGU (or across modules within a single CGU) is common.
- Across Crates: Inlining code from dependency crates requires the function’s metadata (like MIR) to be available (common for generics or
- Compiler Limits: Even with
#[inline(always)], the compiler uses heuristics and may refuse to inline very large/complex functions to avoid excessive code bloat. - Dynamic Linking Preference (
prefer-dynamic): Requesting dynamic linking at the final executable stage generally does not prevent the compiler from inlining functions from Rust libraries (.rlib) during the compilation phase itself.
Finally, enabling Link-Time Optimization (LTO) can overcome some of these boundary limitations, allowing the compiler/linker to perform more aggressive inlining across crates and codegen units, often at the cost of significantly longer link times.
8.12 Methods and Associated Functions
Rust allows associating functions directly with structs, enums, and traits using impl (implementation) blocks. These associated functions come in two main forms: methods and associated functions (often called “static methods” in other languages).
- Methods: Functions that operate on an instance of a type. Their first parameter is always written as
self,&self, or&mut self. These represent the instance itself, an immutable borrow of the instance, or a mutable borrow, respectively. Methods are called using dot notation (instance.method()).- Note on
SelfType: These parameter forms (self,&self,&mut self) are actually shorthand forself: Self,self: &Self, andself: &mut Self. Here,Self(capital ‘S’) is a special type alias within animplblock that refers to the type the block is implementing (e.g.,Circlewithinimpl Circle { ... }). This shows thatselfparameters still follow the standardparameter: Typesyntax.
- Note on
- Associated Functions: Functions associated with a type but not tied to a specific instance. They do not take
selfas the first parameter. They are called using the type name and::syntax (Type::function()). They are commonly used for constructors or utility functions related to the type.
8.12.1 Defining and Calling Methods and Associated Functions
struct Circle {
radius: f64,
}
// Implementation block for the Circle struct (Here, Self = Circle)
impl Circle {
// Associated function: often used as a constructor.
// Does not take 'self'. Called using Circle::new(...).
pub fn new(radius: f64) -> Self { // 'Self' refers to the type 'Circle'
if radius < 0.0 {
panic!("Radius cannot be negative");
}
Circle { radius }
}
// Method: takes an immutable reference ('self: &Self').
// Called using my_circle.area().
pub fn area(&self) -> f64 { // Short for 'self: &Self' or 'self: &Circle'
std::f64::consts::PI * self.radius * self.radius
}
// Method: takes a mutable reference ('self: &mut Self').
// Called using my_circle.scale(...).
pub fn scale(&mut self, factor: f64) { //Short for 'self: &mut Self' (&mut Circle)
if factor < 0.0 {
panic!("Scale factor cannot be negative");
}
self.radius *= factor;
}
// Method: takes ownership ('self: Self').
// Called using my_circle.consume(). The instance cannot be used afterwards.
pub fn consume(self) { // Short for 'self: Self' or 'self: Circle'
println!("Consuming circle with radius {}", self.radius);
// 'self' (the circle instance) is dropped here.
}
}
fn main() {
// Call associated function (constructor)
let mut my_circle = Circle::new(5.0);
// Call methods using dot notation
println!("Initial Area: {}", my_circle.area());
my_circle.scale(2.0); // Calls the mutable method
println!("Scaled Radius: {}", my_circle.radius);
println!("Scaled Area: {}", my_circle.area());
// Call method that consumes the instance
// my_circle.consume();
// println!("Area after consume: {}", my_circle.area());
// Error: use of moved value 'my_circle'
// Alternative way to call methods (less common):
// Explicitly pass the instance reference.
let radius = 10.0;
let another_circle = Circle::new(radius);
let area = Circle::area(&another_circle); // Equivalent to another_circle.area()
println!("Area of another circle: {}", area);
}
As noted in Section 6.3.1, Rust performs automatic referencing and dereferencing for method calls. When using the dot operator (object.method()), the compiler automatically inserts the appropriate &, &mut, or * to match the method’s self, &self, or &mut self receiver as required.
8.13 Function Overloading (or Lack Thereof)
Some languages allow function overloading, where multiple functions can share the same name but differ in the number or types of their parameters. The compiler selects the correct function based on the arguments provided at the call site.
Rust does not support function overloading in the traditional sense. Within a given scope, all functions must have unique names. You cannot define two functions named process where one takes an i32 and the other takes a &str.
Rust achieves similar goals using other mechanisms:
-
Generics: As seen in Section 8.8, a single generic function can work with multiple types, provided they meet the required trait bounds.
use std::fmt::Display; // One generic function instead of multiple overloaded versions. fn print_value<T: Display>(value: T) { println!("Value: {}", value); } fn main() { print_value(10); // Works with i32 print_value("hello"); // Works with &str print_value(3.14); // Works with f64 } -
Traits: Traits define shared behavior. Different types can implement the same trait, providing their own versions of the methods defined by that trait. This allows calling the same method name (
.draw()in the example below) on different types.trait Draw { fn draw(&self); } struct Button { label: String } struct Icon { name: String } impl Draw for Button { fn draw(&self) { println!("Drawing button: [{}]", self.label); } } impl Draw for Icon { fn draw(&self) { println!("Drawing icon: <{}>", self.name); } } fn main() { let button = Button { label: "Submit".to_string() }; let icon = Icon { name: "Save".to_string() }; button.draw(); // Calls Button's implementation of draw icon.draw(); // Calls Icon's implementation of draw }
While not identical to overloading, generics and traits provide powerful, type-safe ways to achieve polymorphism and code reuse in Rust.
8.14 Type Inference for Function Return Types
Rust’s type inference capabilities are powerful for local variables (let x = 5; infers x is i32), but function signatures generally require explicit type annotations for both parameters and return types.
// Requires explicit parameter types and return type.
fn add(a: i32, b: i32) -> i32 {
a + b
}
One notable exception is when using impl Trait in the return position. This syntax allows you to specify that the function returns some concrete type that implements a particular trait, without having to write out the potentially complex or unnameable concrete type itself (especially useful with closures or iterators).
// This function returns a closure. The exact type of a closure is unnameable.
// 'impl Fn(i32) -> i32' means "returns some type that implements this closure trait".
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
// The closure captures 'x' from its environment.
move |y| x + y
}
fn main() {
let add_five = make_adder(5); // add_five holds the returned closure.
println!("Result of add_five(10): {}", add_five(10)); // Output: 15
let add_ten = make_adder(10);
println!("Result of add_ten(7): {}", add_ten(7)); // Output: 17
}
While impl Trait provides some return type inference flexibility, you still must explicitly declare the trait(s) the returned type implements. Full return type inference like in some functional languages is generally not supported to maintain clarity and aid compile-time analysis.
8.15 Variadic Functions and Macros
C allows variadic functions – functions that can accept a variable number of arguments, like printf or scanf, using the ... syntax and stdarg.h macros.
// C Example (for comparison)
#include <stdio.h>
#include <stdarg.h>
// 'count' indicates how many numbers follow.
void print_ints(int count, ...) {
va_list args;
va_start(args, count); // Initialize args to retrieve arguments after 'count'.
printf("Printing %d integers: ", count);
for (int i = 0; i < count; i++) {
int value = va_arg(args, int); // Retrieve next argument as an int.
printf("%d ", value);
}
va_end(args); // Clean up.
printf("\n");
}
int main() {
print_ints(3, 10, 20, 30); // Call with 3 variable arguments.
print_ints(5, 1, 2, 3, 4, 5); // Call with 5 variable arguments.
return 0;
}
Variadic functions in C are powerful but lack type safety for the variable arguments, which can lead to runtime errors if the types or number of arguments retrieved using va_arg don’t match what was passed.
Rust does not support defining C-style variadic functions directly in safe code. You can call C variadic functions from Rust using FFI (Foreign Function Interface) within an unsafe block, but you cannot define your own safe variadic functions using ....
The idiomatic way to achieve similar functionality in Rust (accepting a varying number of arguments) is through macros. Macros operate at compile time, expanding code based on the arguments provided. They are type-safe and more flexible than C variadics.
// Define a macro named 'print_all'
macro_rules! print_all {
// Match one or more expressions separated by commas
( $( $x:expr ),+ ) => {
// Repeat the following code block for each matched expression '$x'
$(
print!("{} ", $x); // Print each expression
)+
println!(); // Print a newline at the end
};
}
fn main() {
print_all!(1, "hello", true, 3.14); // Call the macro with different types
print_all!(100, 200); // Call with just integers
}
Macros like println! itself are prime examples of this pattern. They provide a type-safe, compile-time mechanism for handling variable arguments, which aligns better with Rust’s safety goals than C-style variadics. Macros are a more advanced topic covered later in the book.
8.16 Summary
This chapter provided a comprehensive look at functions and methods in Rust, contrasting them with C/C++ where relevant. Key takeaways include:
mainFunction: The mandatory entry point, can return()orResult<(), E>.- Definition and Calling: Use
fn, no forward declarations needed within a module. Calls require(). Arguments are comma-separated. - Parameters & Data Passing: Ownership transfer (
T), immutable borrow (&T), mutable borrow (&mut T).Copytypes are copied. Choose based on ownership and modification needs.mut Tparams don’t requiremuton the caller’s variable. - Return Values: Use
-> Type. Implicit return via the last expression (no semicolon) is idiomatic; explicitreturnfor early exits. - Lifetimes: Required when returning references (
&T,&mut T) to ensure validity; Rust prevents returning references to local variables. - Scope: Top-level functions visible within their module (
pubfor external visibility). Nested functions are local to their outer function and cannot capture environment variables. - No Default/Named Parameters: Use
Option<T>or the Builder pattern instead. - Slices & Tuples: Efficient for passing views (
&str,&[T]) or returning multiple values(T, U). Unsized typesstrand[T]exist but are used behind pointers. - Generics: Use
<T: Trait>for type-polymorphic functions, enabling source code reuse with type safety (monomorphized at compile time). - Function Pointers & HOFs:
fn(Args) -> Rettype allows passing functions as data. Higher-order functions accept or return functions/closures. Rust function pointers are safe. - Recursion & TCO: Recursion is supported, but Rust provides no guarantee of Tail Call Optimization (TCO), so deep recursion risks stack overflow. Prefer iteration or explicit stack simulation for potentially unbounded depths.
- Inlining: Compiler optimization (
#[inline]hints) to reduce call overhead and enable further optimizations. Limited by various factors (opt level, call type, boundaries, heuristics). LTO can enable more inlining. - Methods & Associated Functions: Defined in
implblocks. Methods operate on instances (self,&self,&mut self, usingSelftype); associated functions belong to the type (Type::func()), often used for constructors. Auto-referencing simplifies method calls. - No Function Overloading: Use generics or traits for polymorphism.
- Return Type Inference: Limited; explicit return types required except for
impl Trait. - Variadics: No direct support; use macros for type-safe variable argument handling.
- Ignoring Returns: Allowed, but
#[must_use]warns if potentially important values (likeResult) are ignored. Uselet _ = ...;for explicit discard.
Functions and methods are central to structuring Rust code safely and efficiently. Understanding ownership, borrowing, lifetimes, and the various ways functions interact with data forms the bedrock for writing effective Rust programs. Later chapters will build on this foundation, exploring closures, asynchronous functions, and advanced trait patterns.
8.17 Exercises
Click to see the list of suggested exercises
-
Maximum Function Variants
-
Variant 1: Write a function
max_i32that takes twoi32parameters by value and returns the maximum value.fn max_i32(a: i32, b: i32) -> i32 { if a > b { a } else { b } } fn main() { let result = max_i32(3, 7); println!("The maximum is {}", result); // Output: The maximum is 7 } -
Variant 2: Write a function
max_refthat takes references (&i32) to twoi32values and returns a reference (&i32) to the maximum value. Pay attention to lifetimes.// The lifetime 'a indicates that the returned reference is tied to the // shortest lifetime of the input references 'a' and 'b'. fn max_ref<'a>(a: &'a i32, b: &'a i32) -> &'a i32 { if a > b { a } else { b } } fn main() { let x = 5; let y = 10; let result_ref = max_ref(&x, &y); println!("The maximum reference points to: {}", result_ref); // Output: 10 // *result_ref is 10. result_ref is valid as long as x and y are. } -
Variant 3: Write a single generic function
max_genericthat works with any typeTthat can be compared (PartialOrd) and copied (Copy). Test it withi32andf64.use std::cmp::PartialOrd; use std::marker::Copy; // Often implicitly req. by usage, good to be explicit fn max_generic<T: PartialOrd + Copy>(a: T, b: T) -> T { if a > b { a } else { b } } fn main() { let int_max = max_generic(3, 7); let float_max = max_generic(2.5, 1.8); println!("The maximum integer is {}", int_max); // Output: 7 println!("The maximum float is {}", float_max); // Output: 2.5 }
-
-
String Concatenation Write a function
concat_stringsthat takes two string slices (&str) as input and returns a newly allocatedStringcontaining the concatenation of the two.fn concat_strings(s1: &str, s2: &str) -> String { let mut result = String::with_capacity(s1.len()+s2.len()); // Pre-allocate cap. result.push_str(s1); result.push_str(s2); result // Return the new owned String } fn main() { let greeting = "Hello, "; let name = "Rustacean!"; let combined = concat_strings(greeting, name); println!("{}", combined); // Output: Hello, Rustacean! } -
Distance Calculation Define a function
distancethat takes two points as tuples(f64, f64)representing (x, y) coordinates, and returns the Euclidean distance between them as anf64. Recall distance = sqrt((x2-x1)^2 + (y2-y1)^2).fn distance(p1: (f64, f64), p2: (f64, f64)) -> f64 { let dx = p2.0 - p1.0; let dy = p2.1 - p1.1; (dx.powi(2) + dy.powi(2)).sqrt() // Use powi(2) for squaring, then sqrt() } fn main() { let point_a = (0.0, 0.0); let point_b = (3.0, 4.0); let dist = distance(point_a, point_b); println!("Distance between {:?} and {:?} is {}", point_a, point_b, dist); // 5 } -
Array Reversal In-Place Write a function
reverse_slicethat takes a mutable slice ofi32(&mut [i32]) and reverses the order of its elements in place (without creating a new array or vector).fn reverse_slice(slice: &mut [i32]) { let len = slice.len(); if len == 0 { return; } // Handle empty slice let mid = len / 2; for i in 0..mid { // Swap element i with element len - 1 - i slice.swap(i, len - 1 - i); } } fn main() { let mut data1 = [1, 2, 3, 4, 5]; reverse_slice(&mut data1); println!("Reversed data1: {:?}", data1); // Output: [5, 4, 3, 2, 1] let mut data2 = [10, 20, 30, 40]; reverse_slice(&mut data2); println!("Reversed data2: {:?}", data2); // Output: [40, 30, 20, 10] let mut data3: [i32; 0] = []; // Empty slice reverse_slice(&mut data3); println!("Reversed empty: {:?}", data3); // Output: [] } -
Find Element in Slice Write a function
find_indexthat takes a slice ofi32(&[i32]) and a targeti32value. It should returnOption<usize>, containingSome(index)if the target is found, andNoneotherwise. Return the index of the first occurrence.fn find_index(slice: &[i32], target: i32) -> Option<usize> { for (index, &value) in slice.iter().enumerate() { if value == target { return Some(index); // Found it, return early } } None // Went through the whole slice, not found } fn main() { let numbers = [10, 25, 30, 15, 25, 40]; match find_index(&numbers, 30) { Some(idx) => println!("Found 30 at index {}", idx), // Output: Found 30 at index 2 None => println!("30 not found"), } match find_index(&numbers, 25) { Some(idx) => println!("Found 25 at index {}", idx), // Output: Found 25 at index 1 (first occurrence) None => println!("25 not found"), } match find_index(&numbers, 99) { Some(idx) => println!("Found 99 at index {}", idx), None => println!("99 not found"), // Output: 99 not found } }