KAIROS CODERS

Rust Functions: Parameters, Return Values, Expressions, Generics & Scope

user

Rahul

August 28, 2026 at 10:20 PM

View Count: 10

Rust Functions: Parameters, Return Values, Expressions, Generics & Scope

We've learned how Rust stores data and controls program execution.

Now it's time to learn one of the most fundamental building blocks of every serious Rust application:

Functions.

A function lets you package logic into a reusable unit.

Instead of writing:

 

println!("Welcome to Kairos Coders");
println!("Welcome to Kairos Coders");
println!("Welcome to Kairos Coders");

 

you can create:

 

fn welcome() {
    println!("Welcome to Kairos Coders");
}

 

and call it whenever you need:

 

welcome();
welcome();
welcome();

 

But Rust functions go much further than this.

In this article, we'll learn:

  • Function syntax
  • Parameters
  • Arguments
  • Return values
  • Return types
  • Expressions vs statements
  • Implicit returns
  • return
  • Multiple parameters
  • Function scope
  • Blocks
  • Nested functions
  • Passing values to functions
  • Returning values
  • Generic functions
  • Practical examples
  • Common mistakes
  • Function design principles

What Is a Function?

A function is a reusable block of code designed to perform a specific task.

Conceptually:

Input
  ↓
Function
  ↓
Output

 

For example:

Two numbers
    ↓
add()
    ↓
Sum

 

Functions help you:

  • Avoid repetition
  • Organize code
  • Improve readability
  • Test individual pieces of logic
  • Build reusable components
  • Separate responsibilities

Your First Function

Here's a simple Rust function:

 

fn greet() {
    println!("Hello, Rust!");
}

 

To execute it:

 

fn main() {
    greet();
}

fn greet() {
    println!("Hello, Rust!");
}

 

Output:

Hello, Rust!

 

The function is defined using:

fn

 

which stands for function.


Function Structure

The basic structure is:

 

fn function_name() {
    // code
}

 

For example:

 

fn calculate() {
    println!("Calculating...");
}

 

A function has:

fn
 ↓
name
 ↓
()
 ↓
{
    body
}

 


Calling a Function

Defining a function doesn't execute it.

This:

 

fn greet() {
    println!("Hello!");
}

 

only defines the function.

You need to call it:

 

greet();

 

For example:

 

fn main() {
    greet();
}

fn greet() {
    println!("Hello!");
}

 


Why Is main() Special?

You've already been using:

 

fn main() {
}

 

main is the entry point of a normal Rust executable.

When you run:

 

cargo run

 

Rust starts execution from:

 

main()

 

You can think of it as:

Program starts
      ↓
    main()
      ↓
other functions

 


Functions With Parameters

Functions become much more useful when they accept data.

Example:

 

fn greet(name: &str) {
    println!("Hello, {name}!");
}

 

Now:

 

fn main() {
    greet("Rahul");
    greet("Aman");
}

 

Output:

Hello, Rahul!
Hello, Aman!

 


Parameters vs Arguments

These terms are often confused.

In:

 

fn greet(name: &str) {
}

 

name is a parameter.

When we call:

 

greet("Rahul");

 

"Rahul" is an argument.

Think:

Function definition
        ↓
Parameter

Function call
        ↓
Argument

 


Multiple Parameters

A function can accept multiple parameters.

 

fn add(a: i32, b: i32) {
    println!("{}", a + b);
}

 

Call it:

 

fn main() {
    add(10, 20);
}

 

Output:

30

 

The parameters are:

a → i32
b → i32

 


Parameter Types Are Required

Rust requires you to specify parameter types.

This is valid:

 

fn add(a: i32, b: i32) {
    println!("{}", a + b);
}

 

But this is not:

 

fn add(a, b) {
}

 

Rust needs to know the types of function parameters.

This is part of Rust's strong type system.


Functions Can Return Values

A function doesn't have to print something.

It can calculate something and return the result.

 

fn add(a: i32, b: i32) -> i32 {
    a + b
}

 

Then:

 

fn main() {
    let result = add(10, 20);

    println!("{result}");
}

 

Output:

30

 


The -> Syntax

The arrow:

 

->

 

specifies the return type.

For example:

 

fn add(a: i32, b: i32) -> i32 {
    a + b
}

 

means:

Inputs:
i32
i32

Output:
i32

 


Implicit Return

This is one of Rust's most important syntax rules.

Consider:

 

fn add(a: i32, b: i32) -> i32 {
    a + b
}

 

The final expression:

 

a + b

 

is returned automatically.

There is no semicolon.


Semicolon Changes the Meaning

Compare these:

 

fn add(a: i32, b: i32) -> i32 {
    a + b
}

 

and:

 

fn add(a: i32, b: i32) -> i32 {
    a + b;
}

 

The first returns the result.

The second has a statement ending with ;, so it doesn't return the integer expression.

This distinction is extremely important in Rust.


Explicit return

You can also explicitly return a value:

 

fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

 

This works.

However, idiomatic Rust often prefers the expression style:

 

fn add(a: i32, b: i32) -> i32 {
    a + b
}

 

Use explicit return when it improves control flow or readability.


Functions Returning Strings

A function can return many different types.

For example:

 

fn get_message() -> &'static str {
    "Hello from Rust"
}

 

Then:

 

fn main() {
    let message = get_message();

    println!("{message}");
}

 

We'll later explore why the return type contains:

&'static str

 

when we study references and lifetimes.

For now, understand that this function returns a string slice.


Functions Returning Booleans

 

fn is_even(number: i32) -> bool {
    number % 2 == 0
}

 

Usage:

 

fn main() {
    let number = 10;

    if is_even(number) {
        println!("Even");
    } else {
        println!("Odd");
    }
}

 

Output:

Even

 

Functions can therefore integrate naturally with control flow.


Functions Returning Tuples

A function can return multiple values using a tuple.

 

fn calculate(a: i32, b: i32) -> (i32, i32) {
    (a + b, a * b)
}

 

Usage:

 

fn main() {
    let (sum, product) = calculate(10, 5);

    println!("Sum: {sum}");
    println!("Product: {product}");
}

 

Output:

Sum: 15
Product: 50

 

This becomes useful when a function naturally produces multiple related results.


Functions Can Return Arrays

A function can also return arrays when the type and ownership/lifetime requirements are appropriate.

For example:

 

fn numbers() -> [i32; 3] {
    [10, 20, 30]
}

 

Usage:

 

fn main() {
    let values = numbers();

    println!("{}", values[0]);
}

 


Expressions vs Statements

Understanding this distinction is essential for writing idiomatic Rust.

A statement performs an action.

An expression produces a value.

For example:

 

let x = 10;

 

is a statement.

While:

 

10 + 20

 

is an expression because it produces:

30

 


Blocks Are Expressions

Even a block can produce a value.

 

let result = {
    let a = 10;
    let b = 20;

    a + b
};

 

The block produces:

30

 

Notice the final expression:

 

a + b

 

has no semicolon.


Functions Use This Design

That's why this works:

 

fn calculate() -> i32 {
    let a = 10;
    let b = 20;

    a + b
}

 

The function body is a block.

Its final expression becomes the return value.

This style is one of the things that makes Rust code concise without sacrificing explicitness.


Scope Inside Functions

Variables declared inside a function are local to that function.

 

fn calculate() {
    let number = 10;

    println!("{number}");
}

 

You cannot access number outside the function.

This won't work:

 

fn calculate() {
    let number = 10;
}

fn main() {
    println!("{number}");
}

 

The variable belongs to the scope of calculate.


Function Scope Example

 

fn first() {
    let value = 100;

    println!("{value}");
}

fn second() {
    let value = 200;

    println!("{value}");
}

 

Both functions can have a variable named value.

They are different variables because they belong to different scopes.


Blocks Create Scope

Rust blocks also create scope.

 

fn main() {
    let outer = 10;

    {
        let inner = 20;

        println!("{outer}");
        println!("{inner}");
    }

    println!("{outer}");
}

 

But:

 

println!("{inner}");

 

outside the block would be invalid.

The inner variable only exists inside its block.


Functions Don't Need to Be Defined Before main

This works:

 

fn main() {
    greet();
}

fn greet() {
    println!("Hello!");
}

 

Rust doesn't require the function definition to appear before its call.

This allows you to organize code logically.


A Function Can Call Another Function

Functions can call other functions.

 

fn main() {
    start();
}

fn start() {
    println!("Starting...");

    initialize();
}

fn initialize() {
    println!("Initializing...");
}

 

The execution flow is:

main()
  ↓
start()
  ↓
initialize()

 

This is how larger applications are built from smaller pieces.


Function Composition

You can combine functions together.

 

fn square(number: i32) -> i32 {
    number * number
}

fn double(number: i32) -> i32 {
    number * 2
}

fn main() {
    let result = double(square(5));

    println!("{result}");
}

 

Execution:

5
 ↓
square()
 ↓
25
 ↓
double()
 ↓
50

 

Output:

50

 

This is the foundation of function composition.


Generic Functions

Now we're moving toward intermediate Rust.

Suppose you want a function that works with multiple types.

Rust provides generics.

For example:

 

fn identity<T>(value: T) -> T {
    value
}

 

Here:

T

 

is a generic type parameter.

You can use it with different types.

 

fn main() {
    let number = identity(10);
    let text = identity("Rust");

    println!("{number}");
    println!("{text}");
}

 

The same function can work with different types.


Understanding <T>

In:

 

fn identity<T>(value: T) -> T {
    value
}

 

T represents an arbitrary type.

Conceptually:

identity<i32>
identity<&str>
identity<String>
identity<YourStruct>

 

depending on what you pass to it.

Rust's compiler determines the concrete type when the function is used.


Generic Function Example

Consider:

 

fn first<T>(value: T) -> T {
    value
}

 

You could use:

 

let number = first(100);
let name = first("Rahul");
let active = first(true);

 

The same generic function works with all of them.


Generic Functions With Multiple Types

You can have more than one generic parameter.

 

fn pair<T, U>(first: T, second: U) -> (T, U) {
    (first, second)
}

 

Usage:

 

fn main() {
    let result = pair(10, "Rust");

    println!("{}", result.0);
    println!("{}", result.1);
}

 

Here:

T → i32
U → &str

 

The function accepts two different types.


Generic Functions and Type Safety

Generics don't mean Rust becomes dynamically typed.

The compiler still knows the concrete types.

For example:

 

let result = pair(10, "Rust");

 

is compiled with the appropriate concrete types.

This provides:

  • Type safety
  • Reusability
  • Compile-time checking
  • Performance

Rust generics are heavily used throughout the standard library.


Functions and Ownership

Here's where Rust starts becoming fundamentally different from many other languages.

Consider:

 

fn consume(value: String) {
    println!("{value}");
}

fn main() {
    let name = String::from("Rust");

    consume(name);

    // println!("{name}"); 
}

 

After passing name into consume, ownership of the String has moved.

Therefore, you can't use name afterward.

This is called move semantics.

Don't worry if this feels unfamiliar.

Ownership is the next major stage of our Rust series.


Passing by Reference

Instead of transferring ownership, you can borrow a value.

 

fn print_name(name: &String) {
    println!("{name}");
}

fn main() {
    let name = String::from("Rust");

    print_name(&name);

    println!("{name}");
}

 

Here:

 

&name

 

passes a reference.

The function can use the value without taking ownership.

We'll explore borrowing and references in depth later.


Prefer &str When Appropriate

For functions that only need to read string data, you will often see:

 

fn greet(name: &str) {
    println!("Hello, {name}!");
}

 

This is flexible because both string literals and borrowed String values can often be passed to it.

For example:

 

greet("Rust");

 

and:

 

let name = String::from("Rahul");
greet(&name);

 

This is an important idiom in Rust.


Function Returning a Reference

You can return references, but then Rust's borrowing and lifetime rules become important.

For example:

 

fn first_character(text: &str) -> Option<char> {
    text.chars().next()
}

 

This function doesn't return a reference; it returns an Option<char>.

That's often convenient when extracting a value.

Returning actual references requires careful lifetime reasoning, which we'll cover later.


Functions With Result

As you advance in Rust, you'll frequently see functions return:

 

Result<T, E>

 

For example:

 

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Cannot divide by zero"))
    } else {
        Ok(a / b)
    }
}

 

Usage:

 

fn main() {
    match divide(10.0, 2.0) {
        Ok(result) => println!("Result: {result}"),
        Err(error) => println!("Error: {error}"),
    }
}

 

Output:

Result: 5

 

Result will become one of the most important types in your professional Rust career.


Function Naming Conventions

Rust conventionally uses snake_case for function names.

Good:

 

calculate_total()
get_user()
process_payment()
create_account()

 

Avoid:

 

calculateTotal()
GetUser()
ProcessPayment()

 

Rust's ecosystem consistently follows snake_case.


Functions Should Have One Clear Responsibility

A good function usually does one meaningful thing.

Instead of:

 

process_everything()

 

consider separating:

 

validate_user()
calculate_total()
save_order()
send_email()

 

Then compose them.

This makes code:

  • Easier to understand
  • Easier to test
  • Easier to modify
  • Easier to debug

Practical Example: Calculator

Let's build a small calculator using functions.

 

fn add(a: f64, b: f64) -> f64 {
    a + b
}

fn subtract(a: f64, b: f64) -> f64 {
    a - b
}

fn multiply(a: f64, b: f64) -> f64 {
    a * b
}

fn divide(a: f64, b: f64) -> f64 {
    a / b
}

fn main() {
    let a = 20.0;
    let b = 5.0;

    println!("Addition: {}", add(a, b));
    println!("Subtraction: {}", subtract(a, b));
    println!("Multiplication: {}", multiply(a, b));
    println!("Division: {}", divide(a, b));
}

 

Output:

Addition: 25
Subtraction: 15
Multiplication: 100
Division: 4

 

Now each operation has its own responsibility.


Practical Example: Temperature Converter

 

fn celsius_to_fahrenheit(celsius: f64) -> f64 {
    (celsius * 9.0 / 5.0) + 32.0
}

fn main() {
    let celsius = 25.0;

    let fahrenheit = celsius_to_fahrenheit(celsius);

    println!("{celsius}°C = {fahrenheit}°F");
}

 

Output:

25°C = 77°F

 


Practical Example: Find Maximum

Let's create a function that compares two numbers.

 

fn maximum(a: i32, b: i32) -> i32 {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    let result = maximum(50, 80);

    println!("Maximum: {result}");
}

 

Output:

Maximum: 80

 

Notice how the if expression directly becomes the function's return value.


Practical Example: Factorial

Functions and loops work naturally together.

 

fn factorial(number: u64) -> u64 {
    let mut result = 1;

    for i in 1..=number {
        result *= i;
    }

    result
}

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

 

Output:

120

 

This kind of function will become useful when we start the algorithm portion of the series.


Practical Example: Generic Identity

Let's revisit generics with something simple:

 

fn identity<T>(value: T) -> T {
    value
}

fn main() {
    let number = identity(42);
    let text = identity("Hello");

    println!("{number}");
    println!("{text}");
}

 

One function supports multiple types.


Common Beginner Mistakes

Mistake 1 — Forgetting the Return Type

If your function returns a value, specify the type:

 

fn add(a: i32, b: i32) -> i32 {
    a + b
}

 


Mistake 2 — Adding a Semicolon to the Final Expression

Incorrect:

 

fn add(a: i32, b: i32) -> i32 {
    a + b;
}

 

Correct:

 

fn add(a: i32, b: i32) -> i32 {
    a + b
}

 


Mistake 3 — Forgetting Arguments

If the function requires:

 

fn greet(name: &str) {
}

 

you can't call:

 

greet();

 

You need:

 

greet("Rust");

 


Mistake 4 — Wrong Argument Type

If you define:

 

fn double(number: i32) -> i32 {
    number * 2
}

 

this isn't valid:

 

double("10");

 

The function expects an i32.


Mistake 5 — Expecting Ownership to Work Like Garbage-Collected Languages

This:

 

let name = String::from("Rust");

consume(name);

println!("{name}");

 

can fail because name was moved.

Understanding this behavior is critical before building larger Rust applications.


Practice Exercises

Exercise 1 — Greeting Function

Create:

 

fn greet(name: &str)

 

that prints:

Hello, <name>!

 


Exercise 2 — Square

Create a function:

square(number)

 

that returns the square of a number.


Exercise 3 — Even Checker

Create:

is_even(number)

 

that returns true if the number is even.


Exercise 4 — Maximum

Create a function that accepts three integers and returns the largest one.


Exercise 5 — Calculator

Create separate functions for:

add
subtract
multiply
divide

 


Exercise 6 — Factorial

Write a function:

factorial(n)

 

that calculates n!.


Exercise 7 — Generic Identity

Create:

 

fn identity<T>(value: T) -> T

 

and test it with:

  • Integer
  • Boolean
  • String slice

Exercise 8 — Temperature

Create functions for:

Celsius → Fahrenheit
Fahrenheit → Celsius

 


A Mental Model for Functions

Think of functions like machines.

              ┌──────────────────┐
Input ───────→│     FUNCTION     │──────→ Output
              └──────────────────┘

 

For example:

10 ──┐
     ├──→ add() ──→ 30
20 ──┘

 

With generics:

Any Type
   ↓
generic function
   ↓
same type

 

With borrowing:

Value
  ↓
Reference
  ↓
Function
  ↓
Value remains owned by caller

 

With ownership:

Value
  ↓
Function
  ↓
Ownership moves

 

Understanding these models will make the upcoming ownership chapters much easier.

Pixels to Perfection Design that Impresses

Want to partner with us? let's innovate together