Create your own
Lesson illustration

Functions with Parameters and Return Values

Hello! Welcome back to your journey into Rust for Solana development.

In our last lesson, we covered Rust's fundamental data types. You learned how to define variables holding single values with scalar types (like i32, bool) and grouped values with compound types (like tuples and arrays). These are the "nouns" of our programs—the data we want to work with.

Today, we'll focus on the "verbs": functions. Functions are the primary way we organize code, create reusable logic, and operate on the data types you've just learned. Your extensive experience with JavaScript and TypeScript means you're already very familiar with the concept of functions. Rust uses them for the same purpose, but with a few key differences that are central to its design philosophy of safety and expressiveness.

Our goal for this lesson is to learn how to define and call functions with parameters and return values, paying close attention to Rust's strict typing and its powerful expression-based syntax.

A Quick Introduction to Rust Functions

Let's start with a very quick video that flies through the core concepts we'll be covering today: defining a function, adding arguments, and returning a value.

3. Creating functions in Rust

This short video from the '300 seconds of Rust' channel provides a great high-level overview of creating and using functions in Rust.

Watch the entire video (it's less than 3 minutes). Don't worry about understanding every detail of the syntax just yet; focus on getting a feel for the overall structure.

The Anatomy of a Function

As you saw in the video, the basic syntax for a function is straightforward. For a more detailed breakdown, the official Rust book is our go-to resource.

Functions - The Rust Programming Language

Let's read the first part of the 'Functions' chapter from 'The Rust Programming Language'. This section covers the basic syntax and naming conventions.

Read the initial section titled 'Functions'. It introduces the fn keyword and the snake_case naming convention.

To summarize, you define a function with the fn keyword, followed by the function's name (by convention, in snake_case), a set of parentheses (), and a code block enclosed in curly braces {}.

Here is a simple example:

fn main() {
    println!("This message is from the main function.");
    call_me(); // We're calling our other function here
}

fn call_me() {
    println!("This message is from the call_me function!");
}

Just like in JavaScript, you can define your functions in any order; Rust's compiler will find them as long as they're in scope.

Making Functions Useful: Parameters

Functions become truly powerful when they can operate on data we pass to them. This is done through parameters.

This is the first major point where Rust's approach differs from JavaScript's. In Rust, you must declare the data type of every function parameter. This isn't optional; it's a core part of the language's commitment to type safety.

Functions - The Rust Programming Language

Let's continue with the Rust book to see how to define parameters.

Read the section titled 'Parameters'. Pay close attention to the syntax for declaring parameters and their types, both for a single parameter and for multiple parameters.

For example, a function that takes two 32-bit signed integers and prints their sum would look like this:

fn print_sum(a: i32, b: i32) {
    let sum = a + b;
    println!("The sum of {} and {} is {}", a, b, sum);
}

fn main() {
    print_sum(10, 20); // Prints "The sum of 10 and 20 is 30"
}

If you try to call print_sum("10", "20"), the compiler will immediately stop you with an error, preventing a whole class of bugs that can occur in dynamically typed languages.

Test your understanding!

You need to write a function that will check if a user is authorized. The function needs two pieces of information: the user's ID (which is a 64-bit unsigned integer) and whether they are an administrator (a boolean).

What would the function signature look like? (You don't need to write the function body).

Show answer

The function signature would be:
fn check_authorization(user_id: u64, is_admin: bool)

This clearly states the function's name, its parameters, and their required types.

Getting Values Back: Return Values

Functions not only take data in, but they can also produce data as output. To understand how Rust handles return values, we first need to grasp a crucial concept: the difference between statements and expressions.

Statements vs. Expressions

This is one of the most important concepts for writing idiomatic Rust.

  • Statements are instructions that perform an action but do not return a value. They end with a semicolon. let x = 5; is a statement.
  • Expressions evaluate to a resulting value. 5 + 6 is an expression that evaluates to 11. Crucially, in Rust, code blocks {} can also be expressions.

Functions - The Rust Programming Language

The next section in the Rust book is essential. It explains the distinction between statements and expressions, which is fundamental to how Rust functions work.

Read the section 'Statements and Expressions'. Focus on how a semicolon can turn an expression into a statement, and how a block of code can evaluate to a value.

The idea that a block of code can evaluate to a value might remind you of Immediately Invoked Function Expressions (IIFEs) in JavaScript, where you might write const result = (() => { /*... code ...*/; return finalValue; })();. Rust integrates this concept much more deeply into the language.

This short video clip provides a great visual summary of this idea.

Rust for the impatient

This clip from 'Rust for the impatient' effectively explains how blocks are expressions and how the final expression becomes the block's value.

Watch from 03:13 to 03:45. Notice the explanation of the 'tail' expression and how omitting the semicolon is equivalent to returning a value.

Returning a Value from a Function

Now that you understand expressions, returning a value is straightforward.

  1. You declare the return type after an arrow ->.
  2. The value of the last expression in the function's body is automatically returned. No return keyword and no semicolon are needed!

This is the idiomatic way to return a value in Rust.

Let's read the final section from the Rust book chapter, which puts this all together.

Functions - The Rust Programming Language

This section demonstrates how to use the -> syntax and the final expression to return a value.

Read the section 'Functions with Return Values'. The plus_one example is especially important. Note the compiler error it shows when a semicolon is incorrectly added, turning the expression into a statement.

Here is an example that combines parameters and a return value:

fn multiply(x: i32, y: i32) -> i32 {
    x * y // This is an expression. Its value is returned.
}

fn main() {
    let product = multiply(8, 7);
    println!("The product is: {}", product); // Prints "The product is: 56"
}

Of course, you can still use the return keyword for an early return, such as in a guard clause.

fn divide(numerator: f64, denominator: f64) -> f64 {
    if denominator == 0.0 {
        return 0.0; // Early return to prevent division by zero
    }
    
    numerator / denominator // Implicit return for the normal case
}
Test your understanding!

Write a complete function named calculate_area that takes two parameters, width and height (both 64-bit floating-point numbers), and returns their area. Use the idiomatic Rust approach for the return value.

Show answer
fn calculate_area(width: f64, height: f64) -> f64 {
    width * height
}

This function correctly defines the types for its parameters (f64), specifies the return type (-> f64), and uses the final expression width * height (without a semicolon) as its return value.

Conclusion

Excellent work! You've now mastered the fundamentals of creating and using functions in Rust. This is a huge step forward, as functions are the backbone of any program you'll write, from a simple command-line tool to a complex Solana smart contract.

Let's recap the key takeaways:

  • Functions are declared with the fn keyword and named using snake_case.
  • You must declare the data type of every function parameter.
  • The return type of a function is specified after an arrow (->).
  • Rust is an expression-based language. The value of the final expression in a function's body is implicitly returned.
  • Adding a semicolon to an expression turns it into a statement, which does not return a value and can lead to compiler errors if a return value is expected.
  • The return keyword is used for exiting a function early, not for the standard return path.

In all our examples today, we passed simple types like i32 and f64. These types are cheap to copy. But what happens when we want to pass more complex, larger data like a String or a user-defined data structure into a function? Do we copy it every time? This question leads directly to Rust's most unique and powerful feature: Ownership. In the next lesson, we will explore this system, which is how Rust guarantees memory safety without a garbage collector.

Can't find a good explanation? Sign up and we'll make it for you

Sign up