Functional Aspect of Rust

Hello friends, we already knew that functional programming is at the peak in a world of the programming language because of its special principle: “functions should be first class values”. In functional programming :

  • functions can be passed as values in other functions
  • one function can return another function
  • a function can be assigned to a variable for later execution, and so forth.

But Rust has benefits of both functional programming and object-oriented programming. In this blog, we will discuss “How Rust implements the features of functional programming in its context”.

The solution is Closures and Iterators which turns rust to be a functional one.

  • Closures, a function-like construct you can store in a variable.
  • Iterators, a way of processing a series of elements.

Other Rust features, such as pattern matching and enums are influenced by the functional style as well.

CLOSURES 

-> Closures are an anonymous function which can be stored in a variable or pass as arguments to other function.

Let us take an example that we have to make an app which generates custom exercise workout plan. We will focus on:

  • “calculation should take a few seconds” instead of working of the algorithm.
  • the algorithm should be called only once and when it is needed
use std::thread;
use std::time::Duration;

fn simulated_expensive_calculation(intensity: u32) -> u32 {
    println!("calculating slowly...");
    thread::sleep(Duration::from_secs(2));
    intensity
}
 fn generate_workout(intensity: u32, random_number: u32) {
    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            simulated_expensive_calculation(intensity)
        );
        println!(
            "Next, do {} sit-ups!",
            simulated_expensive_calculation(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                simulated_expensive_calculation(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(
        simulated_user_specified_value,
        simulated_random_number
    );
}

In this example, we can see the problem with the first If block, there are two calls to the function simulated_expensive_calculation with the same parameter value but the result of the function will not change, hence why to call it twice.

Refactoring using functions

Now the solution for the above problem is to store the function’s result in a variable and use that variable wherever it is necessary.

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_result =
        simulated_expensive_calculation(intensity);

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result
        );
        println!(
            "Next, do {} sit-ups!",
            expensive_result
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result
            );
        }
    }
}

We can see in the above example that when random_number == 3 then it does not use the function, still, the function will be evaluated as that is the first call that we are doing and stored in a variable. To resolve the above problem the closures can be used because closures will not get evaluated until it is needed.

Refactoring using closures

To define a closure, we start with a pair of vertical pipes (|), inside which we specify the parameters to the closure and multiple parameters can be passed using comma separated e.g. the closure definition |num| comes after the = sign and assign it to the variable expensive_closure. The definition or the body of the closure comes in {}.

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_closure(intensity)
        );
        println!(
            "Next, do {} sit-ups!",
            expensive_closure(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

We have solved the problem of evaluating the function irrespective of their usage. But we have again introduced the problem where if block is still calling it twice with the same parameter value.

Solution: We are calling the algorithm stored in the closure and after evaluation, it is returning a result to be used, but the problem is that we are calling the above expensive algorithm again with same parameters for the same results. It will be great if there is a way where we can cache the result of the closure and can use the same result without evaluating the algorithm.  Also, it will help to make it generic where we are able to cache the results with new parameters as well for future purposes.

So here we will make a struct Cacher with “Fn trait” bound so that it takes a parameter of integer and give the result of an integer type. The calculation will store the closure and value will hold the result of closure wrapped in Some or None.

So we implement cacher with two methods :

  • new(), used to create an instance of Cacher
  • value(), when we will call the closure very first time then it will calculate the value of closure and return the result wrapped in Some and when the call will go to the closure for the second time with the same parameter then it will fetch the value stored in Cacher and return that as result. But if we are calling the closure with new parameters then it will again recalculate the closures’ value and cache the result for future purpose and return us the result.
struct Cacher
    where T: Fn(u32) -> u32
{
    calculation: T,
    value: Option<u32>,
}
impl Cacher
    where T: Fn(u32) -> u32
{
    fn new(calculation: T) -> Cacher {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self, arg: u32) -> u32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            },
        }
    }
}
fn generate_workout(intensity: u32, random_number: u32) {
    let mut expensive_result = Cacher::new(|num| {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    });

    if intensity < 25 {
        println!(
            "Today, do {} pushups!",
            expensive_result.value(intensity)
        );
        println!(
            "Next, do {} sit-ups!",
            expensive_result.value(intensity)
        );
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_result.value(intensity)
            );
        }
    }
}

->Closures don’t require type annotation of a parameter or return value like functions do because :

  • functions are part of an explicit interface exposed to our users and they have to agree on what type of value can be passed and what will be the result type
  • but closures are not part of the explicit interface. They are exposed to only our library’s users.
fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

The first line shows a function definition, and the second line shows a fully annotated closure definition. The third line removes the type annotations from the closure definition, and the fourth line removes the brackets, which are optional because the closure body has only one expression. These are all valid definitions that will produce the same behavior when they’re called.

-> First time when we call closure, the compiler will infer the types of parameters and return values and will lock them to closure. So when we call it the second time with the different type of parameter it will give an error of “mismatched type error“.

example:  let example_closure = |x| x;

         let s = example_closure(String::from("hello"));
         let n = example_closure(5);

The compiler gives an error: 

error[E0308]: mismatched types
--> src/main.rs
|
| let n = example_closure(5);
| ^ expected struct `std::string::String`, found
integral variable
|
= note: expected type `std::string::String`
found type `{integer}`

->Closures can capture the environment:

a). closures

fn main() {
    let x = 4;
    let equal_to_x = |z| z == x;
    let y = 4;
    assert!(equal_to_x(y));
}

b). functions

fn main() {
    let x = 4;
    fn equal_to_x(z: i32) -> bool { z == x }
    let y = 4;
    assert!(equal_to_x(y));
}

In example (b), there is an error in the 2nd line as we are using x variable to compare the value of z but x is not passed as parameter to the function but in example(a), closures have the ability to capture the value from its environment i.e x is defined in 1st line so there is no error in the first example.

ITERATORS

  • Iterators allow us to process some task on a sequence of elements.
  • They are lazily evaluated means “they have no effect until you call methods that consume the iterator to use it up”.
let v1 = vec![1, 2, 3];

let v1_iter = v1.iter();

for val in v1_iter {
    println!("Got: {}", val);
}

In the above example, the iterator is stored in the v1_iter variable, and no iteration takes place at that time. When the for loop is called using the iterator in v1_iter, each element in the vec will be printed one by one using iterator.

In languages that don’t have iterators provided by their standard libraries, you would likely write this same functionality by starting a variable at index 0, using that variable to index into the vector to get a value, and incrementing the variable value in a loop until it reached the total number of items in the vector.

  • All iterators implement the Iterator trait which has associated type, item type should be the return type of next method.
trait Iterator {
    type Item;
    fn next(&mut self) -> Option;
}
  • methods can consume iterators like next, sum.

a). next consumes each element by element in the iterator.

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];
    let mut v1_iter = v1.iter();

    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}

b). sum iterates over each element and return the sum of all elements stored in vec.

#[test]
fn iterator_sum() {
    let v1 = vec![1, 2, 3];
    let v1_iter = v1.iter();

    let total: i32 = v1_iter.sum();

    assert_eq!(total, 6);
}
  • methods that can produce other iterators like map and filter.
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);

In the above example ,iter() makes the vec v1 iterable. map takes the closure as parameter and take one element at a time and will return the element increment by 1 .Then it will be collected by collect()and stored in v2.

Summary

Iterators and closures are used a lot in functional programming and above blog introduced their contribution to Rust’s capability to clearly express high-level ideas at the low-level performance. Their implementation does not affect run-time performance, hence, successfully providing zero-cost abstractions.

Written by 

Amita Yadav is a Software Consultant Trainee at Knoldus Software INC, currently working on RUST, system programming language. She is self-determined at what she has to do. She likes to learn trending technologies. She is familiar with languages such as C++, C, Java, Scala and technologies like lagom, Play, Akka, Kafka, spark, and databases like Cassandra, MySql, Sqlite, Slick. She loves to create amazing videos mashup.

3 thoughts on “Functional Aspect of Rust

    1. Hey Shivam, it depends on your algorithm’s logic. At what value of random number, you want to user to take rest like a threshold value. In my case, I have decided 3 ,you can decide any number for random number.

Comments are closed.

Knoldus Pune Careers - Hiring Freshers

Get a head start on your career at Knoldus. Join us!

%d bloggers like this: