Rust and Immutability

side effectsIn this article, we'll look at how Rust enables you to use immutable data structures to improve the performance and security of your applications.

Let's start with syntactic features.

Syntactic features

In Rust, variables are immutable by default. That is, once they are initialized, their value cannot be changed. This is a fundamental aspect of the language that helps prevent many types of errors related to data state. To declare a variable, use the keyword let:

let x = 5;
// x = 6; // это вызовет ошибку компиляции, так как x неизменяемая

If you need to change the value of a variable, you can use a modifier mutwhich explicitly indicates that the variable can be changed:

let mut y = 5;
y = 6; // теперь это корректный код

References in Rust are also immutable by default. That is it is forbidden change the data you are referencing without explicitly stating:

let z = 10;
let r = &z;
// *r = 11; // ошибка, так как r — иммутабельная ссылка

To change data via a link, you need to use a changeable link:

let mut a = 10;
let b = &mut a;
*b = 11; // корректно, так как b — изменяемая ссылка

Data structures in Rust also obey the rules of immutability. If you create an instance of a structure using letall of its fields will be immutable unless each field is explicitly declared as mut:

struct Point {
    x: i32,
    y: i32,
}

let point = Point { x: 0, y: 0 };
// point.x = 5; // ошибка, так как поля структуры неизменяемы

In addition to all this, there are a number of functionalities that facilitate working with immutable data structures. One such feature is the template Creatorwhich allows the structure's data to be modified during its creation, but provides an immutable object as a result:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn new() -> Rectangle {
        Rectangle { width: 0, height: 0 }
    }

    fn set_width(&mut self, width: u32) -> &mut Rectangle {
        self.width = width;
        self
    }

    fn set_height(&mut self, height: u32) -> &mut Rectangle {
        self.height = height;
        self
    }

    fn build(self) -> Rectangle {
        self
    }
}

let rect = Rectangle::new().set_width(10).set_height(20).build();
// rect.width = 15; // ошибка, так как rect неизменяемый после создания

Here Rectangle is created as resizable to adjust its size, but after calling the method build it becomes immutable.

Typical immutable structures

Immutable vectors

In Rust, vectors are mutable by default, but you can use a library like imwhich provides immutable collections. An example of creating and using an immutable vector:

use im::vector::Vector;

fn main() {
    let vec = Vector::new();
    let updated_vec = vec.push_back(42);
    println!("Original vector: {:?}", vec);
    println!("Updated vector: {:?}", updated_vec);
}

Here updated_vec is a new vector containing the added elements, while the original vector vec remains unchanged.

Structural Sharing

Structural sharing allows immutable data structures to share parts of their state with other structures, thereby minimizing the need to copy data. An example can be implemented using the library rpdswhich has persistent data structures:

use rpds::Vector;

fn main() {
    let vec = Vector::new().push_back(10).push_back(20);
    let vec2 = vec.push_back(30);
    println!("vec2 shares structure with vec: {:?}", vec2);
}

vec2 uses most of the data from vecadding only new elements.

Immutable Linked Lists

Immutable linked lists are useful in functional programming. An example of using a persistent linked list:

use im::conslist::ConsList;

fn main() {
    let list = ConsList::new();
    let list = list.cons(1).cons(2).cons(3);
    println!("Persistent list: {:?}", list);
}

Every operation cons creates a new list that contains the new item along with a reference to the previous list.

Immutable hash maps

Immutable hash maps can be used to store and access data by key:

use im::HashMap;

fn main() {
    let mut map = HashMap::new();
    map = map.update("key1", "value1");
    let map2 = map.update("key2", "value2");
    println!("Map1: {:?}", map);
    println!("Map2: {:?}", map2);
}

Here map2 adds a new key-value pair, while map remains unchanged.

Immutable trees

Immutable trees can be used to create complex data structures with search and insert operations:

use im::OrdMap;

fn main() {
    let tree = OrdMap::new();
    let tree = tree.update(1, "a").update(2, "b");
    let tree2 = tree.update(3, "c");
    println!("Tree1: {:?}", tree);
    println!("Tree2: {:?}", tree2);
}

Examples of using

Multithreaded configuration access

Let's develop examples of a system where multiple threads need to access a common configuration without the risk of data races. Immutability is useful here because it ensures that data is not accidentally modified, which we know is very important in a multi-threaded environment.

Let's define an immutable structure AppConfigcontaining configuration parameters:

#[derive(Clone, Debug)]
struct UserState {
    user_id: u32,
    preferences: Vec<String>,
}

Let's create a globally accessible Arc for this configuration to safely share between threads:

impl UserState {
    fn add_preference(&self, preference: String) -> Self {
        let mut new_preferences = self.preferences.clone();
        new_preferences.push(preference);
        UserState {
            user_id: self.user_id,
            preferences: new_preferences,
        }
    }
}

Here, each thread gets secure access to the configuration, which eliminates the possibility of changing it, since the data is protected by immutability and Arc.

Managing State in a Functional Web Application

The second case is a web application where user state is updated without mutations, using FP concepts to improve state manageability and simplify testing.

Let's define an immutable user state structure:

#[derive(Clone, Debug)]
struct UserState {
    user_id: u32,
    preferences: Vec<String>,
}

A state update function that returns a new state:

impl UserState {
    fn add_preference(&self, preference: String) -> Self {
        let mut new_preferences = self.preferences.clone();
        new_preferences.push(preference);
        UserState {
            user_id: self.user_id,
            preferences: new_preferences,
        }
    }
}

Example in the context of request processing:

fn handle_request(current_state: &UserState) -> UserState {
    let updated_state = current_state.add_preference("new_preference".to_string());
    updated_state
}

Here every challenge add_preference creates a new version of the state UserState.

Useful libraries

im — is a high-performance library for working with immutable data structures in Rust. It has a full set of persistent data structures: listsvectors, cards And setswhich preserve previous versions of themselves when modified and allow data to be shared between states without having to copy them entirely.

Example of an immutable list:

use im::ConsList;

fn main() {
    let list = ConsList::new();
    let list = list.cons(1).cons(2).cons(3);
    println!("Persistent list: {:?}", list);
}

Create a list using the method conswhich adds an element to the beginning of a list while preserving the previous version of the list. This is great for functional programs where data immutability is important.

rpdswhich we used just above, provides a collection of immutable and persistent data structures. The library supports the functional style, offering structures that automatically save the history of changes.

An example of using an immutable dictionary:

use rpds::HashTrieMap;

fn main() {
    let map = HashTrieMap::new();
    let map = map.insert("key1", "value1");
    let map2 = map.insert("key2", "value2");
    println!("Map1: {:?}", map);
    println!("Map2: {:?}", map2);
}

Here map2 is created on the basis of map with the addition of a new key-value pair, while the original map remains unchanged.


Thanks to immutability in Rust, you can manage application state without the complexities associated with mutable data structures.

In conclusion, I would like to invite you to free webinarwhere we will take a detailed look at the differences and features of development in Rust for classic backend and for blockchain systems. Registration is available at the link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *