Найти в Дзене
Один Rust не п...Rust

Особенности инкапсуляции в Rust

t.me/oneRustnoqRust - Получить представление об инкапсуляции (Инкапсуляция — это то, что не даёт вам открыть кофе машину и случайно налить туда соль вместо сахара) в Rust для реализации ML. - снизить сложность и облегчить обслуживание и обновление кода. - скрыть детали внутренней реализации структуры или модуля от внешнего кода. - определить четкий интерфейс для взаимодействия внешнего кода с внутренним кодом. - улучшить общий дизайн кода и упростить его использование. - создавать большие программы из более мелких частей. pub и priv, можно использовать для управления видимостью полей структуры, функций модуля и методов. По умолчанию все структурные поля и функции модуля являются частными, и нам нужно использовать ключевое слово pub, чтобы сделать их общедоступными. Для того чтобы структуру было видно извне её модуля, её нужно помечать, как публичную. Пример: struct Person { pub name: String, age: u8, } pub fn add(a: i32, b: i32) -> i32 { a + b } Всё в Rust по умолчани
Оглавление

GitHub - nicktretyakov/parallel_llm_trainer
ML на RUST без заморочек

t.me/oneRustnoqRust

Для чего нужна данная статья? :

- Получить представление об инкапсуляции (Инкапсуляция — это то, что не даёт вам открыть кофе машину и случайно налить туда соль вместо сахара) в Rust для реализации ML.

Зачем Вам это уметь? :

- снизить сложность и облегчить обслуживание и обновление кода.

- скрыть детали внутренней реализации структуры или модуля от внешнего кода.

- определить четкий интерфейс для взаимодействия внешнего кода с внутренним кодом.

- улучшить общий дизайн кода и упростить его использование.

- создавать большие программы из более мелких частей.

Что такое модификаторы видимости?:

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

Пример:

struct Person {

pub name: String,

age: u8,

}

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

a + b

}

Полная инкапсуляция (по умолчанию, private)

Всё в Rust по умолчанию закрыто (private), если явно не указать pub.

struct Model {

weights: Vec<f32>, // Приватное поле

}

impl Model {

fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

fn predict(&self, input: &[f32]) -> f32 {

self.weights.iter().zip(input.iter()).map(|(w, i)| w * i).sum()

}

}

fn main() {

let model = Model::new();

let prediction = model.predict(&[1.0, 2.0, 3.0]);

println!("Prediction: {}", prediction);

}

Где используется

  • Скрытие внутренних данных (например, весов нейросети).
  • Запрет на прямое изменение состояний объектов.

Глобальная инкапсуляция (pub)

Сделает структуру или метод доступным везде.

pub struct Model {

pub weights: Vec<f32>,

}

impl Model {

pub fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

pub fn predict(&self, input: &[f32]) -> f32 {

self.weights.iter().zip(input.iter()).map(|(w, i)| w * i).sum()

}

}

Где используется

  • Открытый API библиотек (pub fn).
  • Доступ к структурам, которые должны быть видны везде.

Инкапсуляция в пределах модуля (pub(crate))

Доступ только внутри текущего крейта.

pub(crate) struct Model {

weights: Vec<f32>,

}

impl Model {

pub(crate) fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

pub(crate) fn predict(&self, input: &[f32]) -> f32 {

self.weights.iter().zip(input.iter()).map(|(w, i)| w * i).sum()

}

}

Где используется

  • Скрытие деталей реализации внутри библиотеки.
  • Разделение API на внешнюю и внутреннюю часть.

Инкапсуляция в пределах родительского модуля (pub(super))

Доступен только в родительском модуле.

mod model {

pub(super) struct Model {

weights: Vec<f32>,

}

impl Model {

pub(super) fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

}

}

fn main() {

// Ошибка: Model не доступен в этом модуле

// let model = model::Model::new();

}

Где используется

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

Инкапсуляция в пределах конкретного модуля (pub(in ...))

Можно ограничить доступ только для определённого модуля.

mod model {

pub(in crate::utils) struct Model {

weights: Vec<f32>,

}

impl Model {

pub(in crate::utils) fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

}

}

mod utils {

use crate::model::Model;

pub fn print_model() {

let model = Model::new();

println!("Model created!");

}

}

Где используется

  • Гранулярный контроль доступа, когда API должно быть доступно только в определённой части кода.

Геттеры и сеттеры (контролируемый доступ)

Можно сделать поля приватными, но добавить pub fn-методы для контроля доступа.

pub struct Model {

weights: Vec<f32>,

}

impl Model {

pub fn new() -> Self {

Self {

weights: vec![0.1, 0.5, 0.9],

}

}

// Геттер

pub fn get_weights(&self) -> &[f32] {

&self.weights

}

// Сеттер с проверкой

pub fn set_weights(&mut self, new_weights: Vec<f32>) {

if new_weights.len() == self.weights.len() {

self.weights = new_weights;

} else {

println!("Ошибка: Неверное количество весов!");

}

}

}

Где используется

  • Когда данные можно просматривать, но нельзя изменять напрямую.
  • Безопасное изменение данных с валидацией.

Инкапсуляция через impl Trait

Позволяет скрыть конкретный тип и возвращать только интерфейс.

trait Model {

fn predict(&self, input: &[f32]) -> f32;

}

struct NeuralNetwork;

impl Model for NeuralNetwork {

fn predict(&self, input: &[f32]) -> f32 {

input.iter().sum() // Заглушка

}

}

fn get_model() -> impl Model {

NeuralNetwork

}

fn main() {

let model = get_model();

let input = [1.0, 2.0, 3.0];

println!("Prediction: {}", model.predict(&input));

}

Где используется

  • Скрытие реализации модели от пользователя.
  • Гибкость при выборе модели.

Инкапсуляция через enum

Можно скрыть детали реализации, предоставляя только высокоуровневый интерфейс.

enum Model {

LinearRegression,

NeuralNetwork,

}

impl Model {

fn predict(&self, input: &[f32]) -> f32 {

match self {

Model::LinearRegression => input.iter().sum(),

Model::NeuralNetwork => input.iter().map(|x| x * 0.5).sum(),

}

}

}

fn main() {

let model = Model::NeuralNetwork;

let input = [1.0, 2.0, 3.0];

println!("Prediction: {}", model.predict(&input));

}

Где используется

  • Полиморфизм без использования трейтов.
  • Скрытие деталей алгоритма внутри одного типа.

Что такое инкапсуляция поведения?:

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

Пример:

pub struct AveragedCollection {

list: Vec<i32>,

average: f64,

}

impl AveragedCollection {

pub fn add(&mut self, value: i32) {

self.list.push(value);

self.update_average();

}

pub fn remove(&mut self) -> Option<i32> {

let result = self.list.pop();

match result {

Some(value) => {

self.update_average();

Some(value)

}

None => None,

}

}

pub fn average(&self) -> f64 {

self.average

}

fn update_average(&mut self) {

let total: i32 = self.list.iter().sum();

self.average = total as f64 / self.list.len() as f64;

}

}

Как написать generic инкапсуляцию?:

Пример:

pub struct AveragedCollection<T> {

list: Vec<T>,

average: f64,

}

impl<T: std::ops::Add<Output = T> + std::ops::Div<Output = T> + Copy>

AveragedCollection<T> {

pub fn new() -> AveragedCollection<T> {

AveragedCollection {

list: Vec::new(),

average: 0.0,

}

}

pub fn add(&mut self, value: T) {

self.list.push(value);

self.update_average();

}

pub fn remove(&mut self) -> Option<T> {

let result = self.list.pop();

match result {

Some(value) => {

self.update_average();

Some(value)

},

None => None,

}

}

pub fn average(&self) -> f64 {

self.average

}

fn update_average(&mut self) {

let total = self.list.iter().fold(T::default(), |a, &b| a + b);

self.average = (total / T::from(self.list.len() as i32).unwrap()).to_f64().unwrap();

}

}

Как написать инкапсуляцию Enum + inner structs?:

Пример:

pub struct AveragedCollection<T, F>

where F: Fn(&Vec<T>) -> f64,

{

list: Vec<T>,

average: f64,

averager: F,

}

impl<T, F> AveragedCollection<T, F>

where F: Fn(&Vec<T>) -> f64,

{

pub fn new(averager: F) -> AveragedCollection<T, F> {

AveragedCollection {

list: Vec::new(),

average: 0.0,

averager,

}

}

pub fn add(&mut self, value: T) {

self.list.push(value);

self.update_average();

}

pub fn remove(&mut self) -> Option<T> {

let result = self.list.pop();

match result {

Some(value) => {

self.update_average();

Some(value)

},

None => None,

}

}

pub fn average(&self) -> f64 {

self.average

}

fn update_average(&mut self) {

self.average = (self.averager)(&self.list);

}

}

Как написать инкапсуляцию Traits + Generics?:

Пример:

pub trait Average {

fn add(&mut self, value: Self::Item);

fn remove(&mut self) -> Option<Self::Item>;

fn average(&self) -> f64;

type Item;

}

pub struct AveragedCollection<T> {

list: Vec<T>,

average: f64,

}

impl<T: std::ops::Add<Output = T> + std::ops::Div<Output = T> + Copy + From<i32>> Average

for AveragedCollection<T>

{

type Item = T;

fn add(&mut self, value: Self::Item) {

self.list.push(value);

self.update_average();

}

fn remove(&mut self) -> Option<Self::Item> {

let result = self.list.pop();

match result {

Some(value) => {

self.update_average();

Some(value)

},

None => None,

}

}
fn average(&self) -> f64 {

self.average

}

}

impl<T: std::ops::Add<Output = T> + std::ops::Div<Output = T> + Copy + From<i32>> AveragedCollection<T> {

pub fn new() -> AveragedCollection<T> {

AveragedCollection {

list: Vec::new(),

average: 0.0,

}

}
fn update_average(&mut self) {

let total = self.list.iter().fold(T::default(), |a, &b| a + b);

self.average = (total / T::from(self.list.len() as i32)).to_f64().unwrap();
}
}

Как использовать модули для контроля доступа.?

Каждый модуль может содержать свои собственные функции, структуры, константы и другие элементы, которые могут быть публичными или приватными. Модули также могут быть вложенными.

mod outer_module {

pub mod inner_module {

pub fn public_function()

{

println!("This is a public function.");

}

fn private_function() {

println!("This is a private function.");

}

}

}

outer_module::inner_module::public_function();

// Доступно outer_module::inner_module::private_function();

Как инкапсулировать через методы доступа (Encapsulation through Accessor Methods)?:

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

struct MyStruct {

field: i32,

}

impl MyStruct

{

pub fn new(val: i32) -> MyStruct

{

MyStruct { field: val }

}

pub fn get_field(&self) -> i32 {

self.field

}

pub fn set_field(&mut self, val: i32) {

self.field = val;

}

}

let mut s = MyStruct::new(10); println!("{}", s.get_field()); // Доступ через геттер

s.set_field(20); // Изменение через сеттер

Способы инкапсуляции мутабельности

Интерьерная мутабельность с помощью Cell<T>

Описание: Cell<T> — это простая абстракция для типов, реализующих трейт Copy (например, i32, bool, f64). Она позволяет изменять внутреннее состояние через неизменяемую ссылку (&T), но без проверки заимствований на этапе компиляции.

Особенности:Использует методы .get() для чтения и .set() для записи.
Не требует заимствований, так как работает с копиями значений.
Подходит для простых типов, где нет необходимости в ссылках.
Пример:

use std::cell::Cell;
let cell = Cell::new(5);

cell.set(10);

println!("{}", cell.get()); // Выведет: 10

Ограничения: Не безопасно для многопоточных сценариев, так как не обеспечивает атомарности операций. Также может привести к ошибкам, если значения перезаписываются без должного контроля.

Интерьерная мутабельность с помощью RefCell<T>

Описание: RefCell<T> позволяет изменять внутреннее состояние через неизменяемую ссылку (&T) для любых типов, не только Copy. В отличие от Cell<T>, она использует runtime-проверку заимствований, что означает, что нарушение правил (например, одновременное наличие нескольких изменяемых заимствований) приведет к панике во время выполнения.

Особенности:Методы .borrow() и .borrow_mut() используются для получения неизменяемой или изменяемой ссылки соответственно.
Подходит для однопоточных сценариев, где компилятор не может гарантировать корректность заимствований на этапе компиляции.
Может быть полезно для кэширования в неизменяемых структурах данных.
Пример:

use std::cell::RefCell;

let refcell = RefCell::new(5);

*refcell.borrow_mut() = 10;

println!("{}", *refcell.borrow()); // Выведет: 10

Ограничения: Не безопасно для многопоточных сценариев, так как runtime-проверка не защищает от гонок данных. Также может привести к панике, если правила заимствования нарушены.

Потокобезопасная мутабельность с помощью Mutex<T>

Описание: Mutex<T> (взаимное исключение) обеспечивает безопасный доступ к изменяемым данным в многопоточных сценариях. Только один поток может получить доступ к данным в любой момент времени, что предотвращает гонки данных.

Особенности:Использует метод .lock() для получения доступа, который блокирует другие потоки до освобождения.
Доступ автоматически освобождается, когда MutexGuard выходит за пределы области видимости.
Есть метод .try_lock() для неблокирующего доступа.
Пример:

use std::sync::Mutex;

let mutex = Mutex::new(5);

let mut guard = mutex.lock().unwrap();

*guard = 10;

println!("{}", *guard); // Выведет: 10

Ограничения: Может привести к панике, если поток пытается заблокировать уже заблокированный Mutex без использования .try_lock(). Производительность может быть ниже по сравнению с однопоточными решениями из-за накладных расходов на синхронизацию.

Потокобезопасная мутабельность с помощью RwLock<T>

Описание: RwLock<T> (чтение-запись замок) позволяет нескольким потокам одновременно читать данные, но только одному потоку писать. Это более эффективно, чем Mutex<T>, в сценариях, где чтение происходит чаще, чем запись.

Особенности:Методы .read() и .write() используются для получения доступа к данным.
Поддерживает неблокирующие версии .try_read() и .try_write().
Доступ автоматически освобождается, когда RwLockReadGuard или RwLockWriteGuard выходит за пределы области видимости.
Пример:

use std::sync::RwLock;

let rwlock = RwLock::new(5);

let read_guard = rwlock.read().unwrap();

println!("{}", *read_guard); // Чтение

let mut write_guard = rwlock.write().unwrap();

*write_guard = 10; // Запись

Ограничения: Производительность может быть ниже, чем у Cell или RefCell, из-за накладных расходов на синхронизацию. Требует осторожности при управлении блокировками для предотвращения дедлоков.

Использование UnsafeCell<T>

Описание: UnsafeCell<T> — это низкоуровневый примитив, который позволяет отключить проверку неизменяемости для неизменяемых ссылок (&T). Это единственный способ в Rust, который позволяет безопасно мутировать данные через общие ссылки, но требует ручной гарантии безопасности.

Особенности:Обычно используется внутри других безопасных абстракций, таких как Cell<T> или RefCell<T>.
Не рекомендуется для прямого использования, так как это unsafe-инструмент, и ошибки могут привести к неопределенному поведению.
Пример:

use std::cell::UnsafeCell;

let unsafe_cell = UnsafeCell::new(5);

unsafe { *unsafe_cell.get() = 10; }

Ограничения: Требует глубокого понимания правил безопасности Rust. Не подходит для большинства сценариев, где есть безопасные альтернативы.

Комбинации для сложных сценариев

Для случаев, когда нужно комбинировать несколько концепций, можно использовать:
Rc<RefCell<T>>: Позволяет иметь несколько владельцев (Rc — Reference Counting) для данных, которые можно изменять через RefCell<T>. Подходит для однопоточных сценариев с разделяемой собственностью.

Пример:

use std::rc::Rc;
use std::cell::RefCell;

let value = Rc::new(RefCell::new(5));

let clone = value.clone();

*clone.borrow_mut() = 10;

Arc<Mutex<T>>: Аналогично, но для многопоточных сценариев, где Arc (Atomic Reference Counting) обеспечивает потокобезопасное совместное владение, а Mutex<T> — изменяемость.

Пример:

use std::sync::{Arc, Mutex};

let value = Arc::new(Mutex::new(5));

let clone = Arc::clone(&value);

let mut guard = clone.lock().unwrap();

*guard = 10;

Рекомендации

  • Выбор метода: Для однопоточных сценариев предпочтительно использовать Cell<T> для простых типов и RefCell<T> для сложных структур. Для многопоточных приложений выбор между Mutex<T> и RwLock<T> зависит от модели доступа: если чтение преобладает, RwLock<T> может быть эффективнее.
  • Производительность: Интерьерная мутабельность (Cell<T>, RefCell<T>) имеет меньшие накладные расходы по сравнению с потокобезопасными механизмами, но не подходит для многопоточных сценариев.
  • Безопасность: Все перечисленные методы, кроме UnsafeCell<T>, предоставляют безопасные API, что минимизирует риск ошибок. Однако разработчикам следует быть внимательными к потенциальным паникам в RefCell<T> или блокировкам в Mutex<T>/RwLock<T>.

Как использовать инкапсуляцию в ML

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

Пример:

pub struct NeuralNetwork {
weights: Vec<f64>, // приватное поле

// другие приватные поля

}

impl NeuralNetwork {

pub fn new() -> Self {

// инициализация

}

pub fn train(&mut self, data: &Vec<Vec<f64>>, labels: &Vec<f64>) {

// обучение модели

}

pub fn predict(&self, input: &Vec<f64>) -> f64 {

// предсказание

}

}

Данные для обучения часто требуют предобработки, например, нормализации или аугментации. Инкапсуляция позволяет скрыть эти детали, предоставляя пользователю простой интерфейс для работы с данными.

Пример:

pub struct Dataset {

data: Vec<Vec<f64>>, // приватное поле

labels: Vec<f64>, // приватное поле

}

impl Dataset {

pub fn from_csv(path: &str) -> Self {

// загрузка и предобработка данных

}

pub fn get_batch(&self, batch_size: usize) -> (Vec<Vec<f64>>, Vec<f64>) {

// возврат мини-пакета данных

}

}

Алгоритмы оптимизации, такие как градиентный спуск, могут быть инкапсулированы с помощью трейтов (интерфейсов), что позволяет скрыть их внутреннюю реализацию и предоставить общий интерфейс.

Пример:

pub trait Optimizer {

fn step(&mut self, gradients: &Vec<f64>);

}

pub struct SGD {

learning_rate: f64, // приватное поле

// другие параметры

}

impl Optimizer for SGD {

fn step(&mut self, gradients: &Vec<f64>) {

// реализация шага оптимизации

}

}

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

Пример:

impl NeuralNetwork {

pub fn save(&self, path: &str) {

// сохранение модели в файл

}

pub fn load(path: &str) -> Self {

// загрузка модели из файла

}

}

В ML-фреймворках, таких как TensorFlow, используются вычислительные графы. В Rust можно инкапсулировать создание и управление этими графами, предоставляя простой API для пользователей.

Пример:

pub struct ComputationGraph {

nodes: Vec<Node>, // приватное поле

// другие компоненты

}

impl ComputationGraph {

pub fn new() -> Self {

// инициализация графа

}

pub fn add_node(&mut self, node: Node) {

// добавление узла в граф

}

pub fn compute(&self, inputs: &Vec<f64>) -> Vec<f64> {

// выполнение вычислений

}

}

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

Пример:

impl NeuralNetwork {

pub fn new(layers: Vec<usize>) -> Result<Self, String> {

if layers.is_empty() {

return Err("Layers cannot be empty".to_string());

}

// инициализация модели

Ok(NeuralNetwork { /* ... */ })

}

}

В ML часто используются параллельные вычисления для ускорения обучения. Инкапсуляция позволяет скрыть детали параллелизма, предоставляя простой интерфейс для пользователя.

Пример:

pub struct ParallelTrainer {

model: NeuralNetwork, // приватное поле

// другие параметры

}

impl ParallelTrainer {

pub fn train(&self, dataset: &Dataset) {

// параллельное обучение модели

}

}