Для чего нужна данная статья? :
- Получить представление об инкапсуляции в Rust.
- Предоставлять общедоступный API только для взаимодействия с 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); // Изменение через сеттер
В некоторых случаях можно полностью скрыть детали реализации типа, предоставляя только его интерфейс, например, через использование трейтов и закрытых структур, что дополнительно усиливает инкапсуляцию.
Можно определить структуры, представляющие модели машинного обучения, где внутренние параметры, такие как веса, являются приватными, а методы для обучения и предсказания — публичными. Это предотвращает случайные изменения внутренних данных и обеспечивает их целостность.
Пример:
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) {
// параллельное обучение модели
}
}