Sage-Code Laboratory
index<--

Function Concept

In programming a function is a named block of code designed to perform a particular task. Functions usually have one or more parameters and most function return one result.The function is a common way to create reusable code and split a larger problem into smaller parts. Rust enable functional programming.
function

Function Concept

Page bookmarks



Define Function

In Rust, a function is defined using the fn keyword followed by the function name, input parameters (if any), and a return type (if any). Here's an example of a simple function in Rust:

Example:

fn main() {
    //test function print_sum() with no result
    print_sum(5, 6); //call function
    print_sum(4, 4); //call function
    //test function call sum_of() two times
    println!("sum_of(5,6) = {}", sum_of(5,6));
    println!("sum_of(4,4) = {}", sum_of(4,4));
    //capture result in new variable z
    let z = sum_of(11, 12);
    println!("sum_of(11, 12): z = {}", z);
}

/* define function with side-effect */
fn print_sum(x: i32, y: i32) {
    println!("print_sum({},{}) = {}", x, y, x + y);
}

/* define function with result */
fn sum_of(x: i32, y: i32) -> i32 {
    return x + y;
}

Notes:

  1. In the function body we can use return statement to create the function result and stop function execution in the same time.
  2. Result of a function, symbol "->" after function parameters and the return statement are both optional.

Homework: Open this example live and run it: functions

Variable Scope

In English the "scope" has two meanings:

This may confuse some for the meaning of "Variable Scope". We use the first meaning: The "scope" of a variable is the area or the block of code where the variable is visible and relevant.

Shadowing

All variables defined in outer scope are visible in the inner scope. We can hide a variable by rebinding the name to a new value using "let". In this case the external variable will be hidden or "shadowed". Variable shadowing can change the type and the value of the variable name.

Examples:

fn main() {
    //outer scope
    let x: i32 = 5;
    let y: u8 = 255;
    { // inner scope
        let x: i64 = 65536;
        println!("inner x = {}",x); // 65536
        println!("outer y = {}",y); // 255
    }
    println!("outer x = {}",x); // 5
}

Notes:

Global & Local scope

The concept of scope in Rust refers to the range or visibility of variables and functions within a program. Rust has two types of scopes: global and local.

Global scope refers to variables and functions that are accessible throughout the entire program, including all functions and blocks within the program. These variables and functions are declared outside of any function, and are usually indicated with the "static" keyword. Global variables and functions can be accessed by any function, regardless of where they are declared within the program.

On the other hand, local scope refers to variables and functions that are only accessible within a specific block or function. These variables and functions are declared within the block or function and can only be accessed within that block or function. Once the block or function is exited, the local variables and functions are no longer accessible.

example:


// global variable
static PI: f32 = 3.14;

fn main() {
    // local variable
    let name = "John";
    println!("Hello, {}, the value of PI is: {}", name, PI);

    sub_function();
}

fn sub_function() {
    // PI variable can also be accessed here
    println!("The value of PI is: {}", PI);

    // local variable
    let age = 25;
    println!("My age is: {}", age);
}

Note: In this example, PI is a global variable that can be accessed within both the main function and the sub_function. On the other hand, name and age are local variables that can only be accessed within their respective functions.

It is important to note that global variables should only be used when necessary, as they can introduce unintended side effects and make the program harder to reason about. It is generally recommended to minimize the use of global variables and instead use local variables whenever possible.

Variadic function

In Rust, variadic functions are functions that can accept a variable number of arguments during runtime. Unlike some other programming languages, such as C and C++, Rust does not have built-in support for variadic functions, but it can be implemented using Rust's macro system.

In Rust, the macro_rules! macro is used to define macros, which are similar to functions, but they operate on the syntax tree instead of values. Using macros, we can define variadic functions by using the ... syntax to represent a variable number of arguments.

Here's an example of how to define a variadic function in Rust using a macro:


macro_rules! print_all {
    ($($arg:expr),*) => {
        $(print!("{} ", $arg);)*
    };
}

Notes In this example, we define a macro called print_all that accepts zero or more arguments of any type. The $($arg:expr),* syntax inside the macro definition matches a comma-separated list of expressions, and the $(print!("{} ", $arg);)* syntax is a loop that prints each expression followed by a space.

We can then use this macro to print a variable number of arguments:

fn main() {
    print_all!(1, 2, 3);
    print_all!("hello", "world");
}

Notes In this example, we call the print_all macro twice, passing in different numbers and types of arguments. Because the macro is defined to accept any number of arguments, both calls will be valid and will produce output.

Run Online

This example can be investigated on-line: Open Example

Using macros to define variadic functions in Rust provides a flexible and powerful way to work with functions that accept a variable number of arguments. However, macros should be used with caution, as they can be more difficult to debug and can be more error-prone than regular functions.

Optional Parameters

In Rust, functions can have optional parameters, which are also known as default parameters. Optional parameters allow a function to be called with fewer arguments than declared, and these parameters take their default values.

When a function is defined, we can specify default values for some or all of its parameters. Here's an example:


fn print_info(name: &str, age: u8, city: &str, country: &str) {
    println!("Name: {}", name);
    println!("Age: {}", age);
    println!("City: {}", city);
    println!("Country: {}", country);
}

fn main() {
    // Call the function with all parameters
    print_info("John", 30, "New York", "United States");

    // Call the function with only some parameters
    print_info("Bob", 25, "San Francisco", "");
}

Notes In this example, the print_info function has four parameters: name, age, city, and country. The age parameter does not have a default value and is required. The other three parameters have default values of empty strings, which means they can be omitted when the function is called.

When the print_info function is called with all four parameters, it prints out all the provided information. When called with only three parameters, it will take the default value of the omitted parameter (country in this example).

Name: John
Age: 30
City: New York
Country: United States

Name: Bob
Age: 25
City: San Francisco
Country:

Run Online

This example can be investigated on-line: Open Example

This is a useful feature in Rust, particularly in cases when a caller may not have some information and would prefer that the function uses a defined default value for omitted parameters. However, note that optional parameters in Rust can only be trailing parameters i.e. those default values must be defined at the end of the parameter list.

Multiple Results

In Rust, a function can return multiple values, and the multiple values are often called a tuple. By default, these values are returned as a tuple without names. However, Rust also provides a feature called named multiple results, which allows developers to give meaningful names to the individual values returned by a function.

Here's an example of a function that returns multiple values:


fn calc_statistics(numbers: &[i32]) -> (i32, i32, f64) {
    let count = numbers.len() as i32;
    let sum = numbers.iter().sum();
    let mean = sum as f64 / count as f64;

    (count, sum, mean)
}

This function calculates the count, sum, and mean of a list of numbers and returns the results as a tuple.

We can call the function and get the tuple of results like this:

let stats = calc_statistics(&[1, 2, 3, 4, 5]);
println!("Count: {}, Sum: {}, Mean: {}", stats.0, stats.1, stats.2);

In this example, we are accessing the values of the tuple using their index position (0 for count, 1 for sum, and 2 for mean).

But we can improve the readability of our code by making use of named multiple results:


fn calc_statistics_for_display(numbers: &[i32]) -> (i32, i32, f64) {
    let count = numbers.len() as i32;
    let sum = numbers.iter().sum();
    let mean = sum as f64 / count as f64;

    (count, sum, mean)
}

fn main() {
    let (count, sum, mean) = calc_statistics_for_display(&[1, 2, 3, 4, 5]);
    println!("Count: {}, Sum: {}, Mean: {}", count, sum, mean);
}

Notes In this example, we are capturing the named tuple components in variables. Now, it is much easier to understand our code and remember which value corresponds to which calculation result.

Run Online

This example can be investigated on-line: Open Example

Anonymous Functions

In Rust, anonymous functions are functions that do not have a name or identifier. Instead, they are assigned to a variable or passed as an argument to another function.

Example 1

Here's an example of an anonymous function being assigned to a variable:


fn main() {
    let sum = |x: i32, y: i32| -> i32 { x + y };
    
    println!("{}", sum(1,2));
    println!("{}", sum(123,7));
}

In this example, the "|x, y|" syntax defines the parameters of the anonymous function, while "i32" after the arrow ("->") indicates the return type. The function then adds "x" and "y" and returns the result.

Run Online

This example can be investigated on-line: Open Example

Example 2

Here's another example of an anonymous function being passed as an argument to a higher-order function:


fn apply_function(f: F, x: i32, y: i32) -> i32
where
  F: Fn(i32, i32) -> i32,
{
  f(x, y)
}

fn main() {
  let sum = |x, y| x + y;

  let result = apply_function(sum, 10, 20);
  println!("Result: {}", result);
}

Notes In this example, the "apply_function" takes an anonymous function f as its first argument, along with two integers x and y. The function then calls f with the two arguments and returns the result. The "sum" function is defined using the same syntax as the previous example.

Run Online

This example can be investigated on-line: Open Example

IIF Functions in Rust

In Rust, IIF (Immediately Invoked Function) functions are self-executing functions that do not need to be explicitly called. They are used to create a new scope for variables and execute code that needs to be run only once.

Here's an example of an IIF function:


(|x: i32, y: i32| -> i32 {
  println!("This code is executed immediately!");
  x + y
})(10, 20);

Notes In this example, we define an anonymous function that accepts two integer arguments and returns their sum. We then immediately invoke the function by surrounding the function definition with parentheses "()" "and providing its arguments "10" and "20" at the end.

The benefit of this is that we can execute some code inside the IIF function and then none of the variables that we've defined inside the function will be available outside of the scope of the function.


(|name: &str| {
  let greeting = format!("Hello, {}!", name);
  println!("{}", greeting);
})("John Doe");

/* The variable `greeting` is not available 
   outside of the IIF function, so this will not compile */
println!("{}", greeting);

In this example, we define an anonymous function that takes a string argument and creates a greeting for that name using a new variable "greeting". Since this variable is only defined within the scope of the IIF function, it is not available outside of it. If we tried to access the "greeting" variable outside of the function scope, we would receive a compile error.

Overall, IIF functions provide a way to execute code once and isolate variables within a new scope without the need for a named function. They can be useful for short-lived functions and for breaking up complex code into smaller, more manageable parts.

Higher-Order Functions

In Rust, functions can be defined as values and passed around as arguments to other functions or returned as results from functions. Functions that take functions as arguments or return functions as results are called higher-order functions.

Example 1:

Here's an example of a higher-order function:


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

fn twice<F: Fn(i32, i32) -> i32>(f: F, arg: i32) -> i32 {
  f(arg, arg) // Calls the function f twice with the same argument
}

let result = twice(add_numbers, 3); // result will be 6

Notes In this example, we define a function "twice" that takes a function "f" and an integer argument "arg". The "twice" function then calls the function "f" twice with the same argument "arg". The "add_numbers" function is defined separately and passed as the first argument to "twice"; this means that "add_numbers" is a higher-order function itself, as it is being passed as an argument.

Example 2:

Here's another example of a higher-order function that returns a function:


fn add_n(n: i32) -> impl Fn(i32) -> i32 {
    move |x| x + n
}

fn main() {
    let add_6 = add_n(6);

    let result1 = add_6(3); // result1 will be 9
    let result2 = add_6(5); // result2 will be 11
    println!("Result 1: {}", result1);
    println!("Result 2: {}", result2);
}

Notes In this example, we define a function "add_n" that takes an integer argument "n" and returns a closure (anonymous function) that takes an integer argument "x" and returns the sum of "x" and "n". We then create a new function "add_6" by calling "add_n(6)", which creates a closure that adds 6 to its argument. We can then call "add_6" with different argument values to get different results.

Run Online

This example can be investigated on-line: Open Example

Callback Functions

In Rust, a callback function is a function that is passed as an argument to another function and is called inside that function. The main purpose of a callback function is to allow the caller function to customize part of its behavior by executing the specialized code defined in the callback function.

Example:

Here's an example program in Rust that uses a callback function:


fn main() {
    let result = operation(100, |x| x * 2);
    println!("Result: {}", result);
}

fn operation(num: i32, callback: impl Fn(i32) -> i32) -> i32 {
    callback(num)
}

In this example, the main function calls the "operation" function with an integer argument "100" and a closure as the second argument. The closure "|x| x * 2" is the callback function which takes an input "x" of type "i32" and returns "x * 2".

The "operation" function accepts two arguments: an "i32" number and a closure "callback" that takes an "i32" argument and returns another "i32". The function then calls the "callback" closure with the input "num" and returns the result.

When the program runs, it passes the closure "|x| x * 2" as the "callback" argument to the "operation" function. The "operation" function calls the closure with "100" as input and returns "200", which is then printed to the console.

Run Online

This example can be investigated on-line: Open Example


Read next: Rust OOP