Rust и иммутабельность

от автора

Привет, Хабр!

Иммутабельность данных в Rust – это основа для создания систем, устойчивых к ошибкам и сайд-эффектам. В этой статье рассмотрим, как Rust позволяет использовать неизменяемые структуры данных для улучшения производительности и безопасности приложений.

Начнем с синтаксических особенностей.

Синтаксические особенности

В Rust переменные по умолчанию иммутабельны. То есть после их инициализации изменить значение нельзя. Это основной аспект языка, который помогает предотвратить множество видов ошибок, связанных с состоянием данных. Для объявления переменной используется ключевое слово let:

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

Если нужно изменить значение переменной, можно использовать модификатор mut, который явно указывает, что переменная может быть изменена:

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

Ссылки в Rust также иммутабельны по умолчанию. То есть нельзя изменить данные, на которые ссылаетесь, без явного указания:

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

Для изменения данных через ссылку нужно использовать изменяемую ссылку:

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

Структуры данных в Rust также подчиняются правилам иммутабельности. Если создается экземпляр структуры с помощью let, все его поля будут неизменяемыми, если только каждое поле явно не объявлено как mut:

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

Помимо всего этого, есть ряд функциональных возможностей, которые способствуют работе с иммутабельными структурами данных. Одна из таких возможностей — это шаблон Создатель, позволяющий изменять данные структуры в процессе её создания, но предоставляя в результате неизменяемый объект:

#[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 неизменяемый после создания

Здесь Rectangle создается как изменяемый для настройки его размеров, но после вызова метода build он становится неизменяемым.

Типичные иммутабельные структуры

Иммутабельные векторы

В Rust векторы по умолчанию являются изменяемыми, но можно использовать библиотеку, такую как im, которая предоставляет иммутабельные коллекции. Пример создания и использования иммутабельного вектора:

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); }

Здесь updated_vec является новым вектором, содержащим добавленные элементы, в то время как оригинальный вектор vec остается неизменным.

Структурный общий доступ

Структурный общий доступ позволяет иммутабельным структурам данных делиться частями своего состояния с другими структурами, минимизируя тем самым необходимость копирования данных. Пример можно реализовать с помощью библиотеки rpds, которая имеет персистентные структуры данных:

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 использует большую часть данных из vec, добавляя только новые элементы.

Иммутабельные связные списки

Иммутабельные связные списки полезны в функциональном программировании. Пример использования персистентного связного списка:

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

Каждая операция cons создает новый список, который содержит новый элемент наряду со ссылкой на предыдущий список.

Иммутабельные хэш-карты

Иммутабельные хэш-карты могут использоваться для хранения и доступа к данным по ключу:

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); }

Здесь map2 добавляет новую пару ключ-значение, при этом map остается неизменной.

Иммутабельные деревья

Иммутабельные деревья можно использовать для создания сложных структур данных с операциями поиска и вставки:

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); }

Примеры использования

Многопоточный доступ к конфигурации

Разработаем примеры системы, где множество потоков должны получать доступ к общей конфигурации без риска гонок данных. Иммутабельность здесь полезна тем, что гарантирует, что данные не будут случайно изменены, что, как мы знаем, очень важно в многопоточном окружении.

Определим иммутабельную структуру AppConfig, содержащую конфигурационные параметры:

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

Создадим глобально доступный Arc для этой конфигурации, чтобы безопасно делиться между потоками:

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,         }     } }

Здесь каждый поток получает безопасный доступ к конфигурации, что исключает возможность её изменения, т.к данные защищены иммутабельностью и Arc.

Управление состоянием в функциональном веб-приложении

Второй кейс — это веб-приложение, где состояние пользователя обновляется без мутаций, используя концепции ФП для улучшения управляемости состояния и упрощения тестирования.

Определим иммутабельную структуру состояния пользователя:

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

Функция обновления состояния, возвращающая новое состояние:

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,         }     } }

Пример в контексте обработки запроса:

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

Здесь каждый вызов add_preference создаёт новую версию состояния UserState.

Полезные библиотеки

im — это высокопроизводительная библиотека для работы с иммутабельными структурами данных в Rust. Она имеет полный набор персистентных структур данных: списки, векторы, карты и множества, которые сохраняют предыдущие версии себя при модификациях и позволяют разделять данные между состояниями без необходимости их полного копирования.

Пример иммутабельного списка:

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

Создаем список с помощью метода cons, который добавляет элемент в начало списка, сохраняя при этом неизменной предыдущую версию списка. Это суперски подходит для функциональных программ, где неизменяемость данных важна.

rpds, которую мы применяли чуть выше, предоставляет коллекцию иммутабельных и персистентных структур данных. Библиотека поддерживает функциональный стиль, предлагая структуры, которые автоматом сохраняют историю изменений.

Пример использования иммутабельного словаря:

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); }

Здесь map2 создается на основе map с добавлением новой пары ключ-значение, при этом оригинальный map остается неизменным.


Благодаря иммутабельности в Rust, можно управлять состоянием приложений, избегая сложностей, связанных с мутабельными структурами данных.

В завершение хочу пригласить вас на бесплатный вебинар, где мы подробно рассмотрим различия и особенности разработки на Rust для классического backend и для блокчейн-систем. Регистрация доступна по ссылке.


ссылка на оригинал статьи https://habr.com/ru/articles/832930/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *