Для чего нужна данная статья? :
- Получить представление о мутабельности полиморфизма в Rust.
- Написать код для задач машинного обучения.
- Найти компромиссы между разными видами и типами полиморфизма.
Зачем Вам это уметь? :
1. Для создания более гибких API - необходимы Вам и тем кто потребляет Ваш код.
2. Повторное использование кода - необходимо Вам и тем кто потребляет Ваш код.
3. Для поиска компромисса между двоичным размером и производительностью.
4. Создать полноценную интеграцию с библиотеками ML, такими как ndarray, tch-rs (bindings для PyTorch), burn, linfa.
Полиморфизм и мутабельность в Rust
Простыми словами: В Rust полиморфизм (работа с разными типами) может зависеть от того, изменяемый объект или нет.
Пример с трейтами:
trait Printable {
fn print(&self); // метод для неизменяемых объектов
fn print_mut(&mut self); // метод для изменяемых объектов
}
struct Text(String);
impl Printable for Text {
fn print(&self) {
println!("{}", self.0);
}
fn print_mut(&mut self) {
self.0 = self.0.to_uppercase();
println!("{}", self.0);
}
}
fn main() {
let text = Text("hello".to_string());
text.print(); // HELLO (не изменит оригинал)
let mut text_mut = Text("hello".to_string());
text_mut.print_mut(); // HELLO (изменит оригинал на "HELLO")
}
Здесь метод print работает с неизменяемыми объектами, а print_mut — с изменяемыми.
Вывод
- Полиморфизм — единый интерфейс для разных типов.
- Мутабельность — можно ли менять объект.
В Rust полиморфизм часто реализуется через трейты, а мутабельность влияет на то, какие методы можно вызывать.
Какие виды полиморфизма в Rust? :
- Статический
- Динамический
Как реализован полиморфизм в Rust? :
Статическая диспетчеризация - использует универсальные признаки, чтобы обеспечить необходимую гибкость, сохраняя при этом безопасность типов и не требуя большого количества дублирующегося кода. По причине создания нескольких копий функции, это создает больший двоичный размер.
Статическая диспетчеризация — это способ выбора конкретной реализации функции (метода) на этапе компиляции программы, а не во время её выполнения. То есть, компилятор Rust заранее знает, какую именно версию функции нужно вызвать, и вставляет её напрямую в код.
Ключевые моменты cтатической диспетчеризации:
- На этапе компиляции — всё решается до запуска программы.
- Без накладных расходов — нет проверок типов во время выполнения, поэтому код работает быстрее.
- Через обобщённые функции (generics) или трейты — Rust использует механизмы, которые позволяют компилятору "развернуть" код для каждого конкретного типа.
Пример: Обобщённые функции (Generics)
Представьте, что у вас есть функция, которая печатает значение любого типа:
fn print_value<T: std::fmt::Display>(value: T) {
println!("Значение: {}", value);
}
- Здесь T — это обобщённый тип.
- Когда вы вызываете print_value(5) или print_value("привет"), компилятор создаёт отдельную версию функции для i32 и для &str.
- На этапе компиляции Rust знает, какую версию использовать — это и есть статическая диспетчеризация.
Пример: Трейты и статическая диспетчеризация
Допустим, у вас есть трейт Animal и две структуры, которые его реализуют:
trait Animal {
fn make_sound(&self);
}
struct Dog;
impl Animal for Dog {
fn make_sound(&self) {
println!("Гав!");
}
}
struct Cat;
impl Animal for Cat {
fn make_sound(&self) {
println!("Мяу!");
}
}
Если вы напишете функцию, которая принимает конкретный тип, реализующий Animal:
fn animal_sound<T: Animal>(animal: T) {
animal.make_sound();
}
- При вызове animal_sound(Dog) или animal_sound(Cat) компилятор подставит нужную реализацию на этапе компиляции.
- Это снова статическая диспетчеризация.
Чем отличается от динамической диспетчеризации?
- Статическая: Быстрее, потому что всё решается при компиляции.
- Динамическая: Использует указатели на трейт-объекты (Box<dyn Animal>), и выбор метода происходит во время выполнения (медленнее, но гибче).
Итог
Статическая диспетчеризация в Rust — это способ сделать код быстрым и безопасным, выбирая нужную реализацию функции на этапе компиляции. Она работает через обобщённые функции и трейты, и не требует дополнительных проверок во время выполнения.
Пример: Компилятор для функции cal, создал один экземпляр для каждого типа. Затем, в каждом из этих экземпляров, он вызывает соответствующий метод.
trait Calc {
fn cal(&self);
}
fn static_dispatch<T: Calc>(t: T) {
t.cal();
}
static_dispatch(Plus{});
static_dispatch(Minus{});
struct Plus;
impl Calc for Plus {
#[inline(never)]
fn cal(&self) {
println!("Plus");
}
}
struct Minus;
impl Calc for Minus {
#[inline(never)]
fn cal(&self) {
println!("Minus");
}}
Что особенного в статическом полиморфизме ?:
Не оказывает влияния на работу функции во время выполнения, но реализуется во время компиляции, следовательно, наиболее эффективен в отношении производительности.
Использует типовые параметры (подкатегория дженериков) - процесс генерации нескольких инстанций функции для разных типовых параметров
Реализуется с помощью ключевого слова impl (по сути, являющего собой анонимные типовые параметры).
Статический полиморфизм — это способность функции или типа работать с разными видами данных на этапе компиляции, без потери производительности. То есть, компилятор заранее знает, какие конкретные типы или реализации будут использоваться, и подставляет их напрямую в код.
Ключевые моменты:
- На этапе компиляции: Всё решается до запуска программы.
- Без накладных расходов: Нет проверок типов во время выполнения (в отличие от динамического полиморфизма).
- Безопасность типов: Rust гарантирует, что все типы совместимы.
Как это работает в Rust?
В Rust статический полиморфизм реализуется через:
- Обобщённые функции (generics)
- Трейты (traits)
Обобщённые функции (generics)
Позволяют писать код, который работает с разными типами данных.
Пример:
fn print_value<T>(value: T)
where
T: std::fmt::Display, // Трейт для вывода на экран
{
println!("Значение: {}", value);
}
fn main() {
print_value(42); // Работает с целым числом
print_value("hello"); // Работает со строкой
}
Здесь T — это обобщённый тип. Компилятор создаст две версии функции: одну для i32, другую для &str.
Трейты (traits)
Трейты — это набор методов, которые тип должен реализовать. Они позволяют абстрагироваться от конкретных типов.
Пример:
trait Greet {
fn greet(&self);
}
struct Person;
impl Greet for Person {
fn greet(&self) {
println!("Привет, я человек!");
}
}
struct Cat;
impl Greet for Cat {
fn greet(&self) {
println!("Мяу!");
}
}
fn say_hello<T: Greet>(item: T) {
item.greet();
}
fn main() {
let person = Person;
let cat = Cat;
say_hello(person); // Привет, я человек!
say_hello(cat); // Мяу!
}
Здесь say_hello работает с любым типом, который реализует трейт Greet.
Почему это важно?
- Производительность: Нет проверок типов во время выполнения.
- Безопасность: Компилятор проверяет совместимость типов.
- Гибкость: Можно писать универсальный код для разных типов.
fn print_static<T: Calc>(to: T) {
println!("{to}");
}
print_static(123);
Типовый параметр объявляется внутри <>, названием его служит T, а Calc - трейт, позволяющий форматировать значения.
Пример:
fn main() {
trait Calc {
fn cal(&self); //реализация одного метода
}
struct Plus;
impl Calc for Plus { //тип, реализующий метод
#[inline(never)]
fn cal(&self) {
println!("Plus");
}
}
struct Minus;
impl Calc for Minus { //тип, реализующий метод
#[inline(never)]
fn cal(&self) {
println!("Minus");
}
}
}
Динамическая диспетчеризация - использует "объекты признаков" для принятия решения о том, какой тип необходим для какого-либо интерфейса к среде выполнения. Это сокращает двоичный размер (так как здесь не используется несколько копий функции), но влечет за собой снижение производительности из-за дополнительного поиска во время выполнения.
Существует отдельный вариант - Крейт, предоставляющий Динамическую диспетчеризацию - enum_dispatch.
Динамическая диспетчеризация — это механизм, который позволяет программе во время выполнения (runtime) определять, какую именно функцию или метод вызывать, в зависимости от типа объекта.
Ключевые моменты:
Динамическая диспетчеризация решает это во время выполнения, используя специальные механизмы (например, trait objects в Rust).
Пример на Rust
Представьте, что у вас есть несколько животных, которые умеют издавать звуки:
trait Animal {
fn make_sound(&self);
}
struct Dog;
impl Animal for Dog {
fn make_sound(&self) {
println!("Гав!");
}
}
struct Cat;
impl Animal for Cat {
fn make_sound(&self) {
println!("Мяу!");
}
}
Динамическая диспетчеризация (trait object):
fn dynamic_dispatch(animal: &dyn Animal) {
animal.make_sound();
}
Здесь &dyn Animal — это trait object, который хранит указатель на объект и таблицу виртуальных методов (vtable). Выбор метода происходит во время выполнения.
Когда использовать динамическую диспетчеризацию?
- Когда вам нужно работать с объектами разных типов, но вы не знаете их заранее.
- Когда вы хотите избежать дублирования кода (например, при работе с коллекциями разных типов).
Минус: небольшая потеря производительности из-за косвенного вызова.
Пример: Вместо дублирования функций создается одна реализация, но эта реализация предназначена для вызова различных типов и методов на основе указателя, вычисляемого во время выполнения.
fn main() {
trait Calc {
fn cal(&self);
}
fn dynamic_dispatch(t: &dyn Calc) { t.cal(); }
dynamic_dispatch(&Plus{});
dynamic_dispatch(&Minus{});
struct Plus;
impl Calc for Plus {
#[inline(never)]
fn cal(&self) {
println!("Plus");
}
}
struct Minus;
impl Calc for Minus {
#[inline(never)]
fn cal(&self) {
println!("Minus");
}
}}
Что особенного в динамическом полиморфизме ?:
Динамический полиморфизм — это способность программы работать с объектами разных типов через единый интерфейс, причём конкретный тип объекта определяется во время выполнения программы (а не на этапе компиляции).
Позволяет сделать функции более гибкими, однако бьёт по производительности.
Реализуется как передача вместе со значением (виртуальная таблица), типом и методом.
Реализуется с помощью ключевого слова dyn. Типы нужно передавать посредством указателя (два указателя - на значение и на динамическую таблицу), ссылки или умного указателя.
Запрещает использование дженериков.
В Rust динамический полиморфизм реализуется с помощью трейтов (traits) и указателей на трейты (trait objects) — обычно это &dyn Trait или Box<dyn Trait>.
Указатели на трейты (Trait Objects)
Чтобы использовать динамический полиморфизм, нужно создать указатель на трейт — например, &dyn Draw или Box<dyn Draw>. Это позволяет хранить в одной переменной объекты разных типов, если они реализуют один и тот же трейт.
struct Circle;
struct Square;
impl Draw for Circle {
fn draw(&self) {
println!("Рисуем круг");
}
}
impl Draw for Square {
fn draw(&self) {
println!("Рисуем квадрат");
}
}
Теперь можно создать коллекцию объектов, реализующих Draw, и вызывать метод draw() для каждого, не зная их конкретного типа:
let shapes: Vec<&dyn Draw> = vec![&Circle, &Square];
for shape in shapes {
shape.draw(); // Вызов правильного метода в зависимости от типа
}
Пример из жизни
Представьте, что у вас есть коробка с карандашами и фломастерами. Вы не знаете, что именно лежит в коробке, но знаете, что всё это можно нарисовать. Вы просто берёте предмет и говорите: "Нарисуй себя!" — и каждый предмет рисует себя по-своему.
Зачем это нужно?
- Гибкость: можно работать с разными типами через один интерфейс.
- Расширяемость: легко добавлять новые типы, реализующие трейт.
- Динамическое поведение: выбор метода происходит во время выполнения.
Пример:
fn print_dynamic(to: &dyn Calc) {
println!("{to}");
}
print_dynamic(&123);
Пример:
fn main() {
trait Calc {
fn cal(&self); //реализация одного метода
}
struct Plus;
impl Calc for Plus { //тип, реализующий метод
#[inline(never)]
fn cal(&self) {
println!("Plus");
}
}
struct Minus;
impl Calc for Minus { //тип, реализующий метод
#[inline(never)]
fn cal(&self) {
println!("Minus");
}
}
}
Что такое Enum полиморфизм?:
Enum — это тип данных, который позволяет определить набор возможных вариантов (значений). Каждый вариант может хранить разные данные.
Enum полиморфизм это возможность работать с разными типами данных через единый интерфейс. В Rust это часто реализуется через enum и match.
Простыми словами:
Вы можете написать код, который будет работать с любым вариантом enum, не зная заранее, какой именно вариант пришёл.
Enum полиморфизм в Rust
Когда вы используете enum для описания разных типов данных и затем обрабатываете их через match, вы реализуете полиморфизм.
Пример:
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("Программа завершена"),
Message::Write(text) => println!("Текст: {}", text),
Message::Move { x, y } => println!("Перемещение на {}, {}", x, y),
Message::ChangeColor(r, g, b) => println!("Цвет: RGB({}, {}, {})", r, g, b),
}
}
Здесь функция process_message может принимать любой вариант Message и обрабатывать его по-разному — это и есть полиморфизм.
Зачем это нужно?
- Единый интерфейс для разных типов данных.
- Безопасность: компилятор проверяет, что вы обработали все варианты.
- Гибкость: можно легко добавлять новые варианты в enum.
Пример из реальной жизни
Представьте, что у вас есть корзина с фруктами:
enum Fruit {
Apple(u32), // Яблоко с количеством семечек
Banana(f32), // Банан с длиной
Orange, // Апельсин без дополнительных данных
}
fn eat(fruit: Fruit) {
match fruit {
Fruit::Apple(seeds) => println!("Съели яблоко с {} семечками", seeds),
Fruit::Banana(length) => println!("Съели банан длиной {} см", length),
Fruit::Orange => println!("Съели апельсин"),
}
}
Здесь eat — полиморфная функция, которая работает с любым фруктом.
Итог
- Enum — это способ описать набор возможных вариантов.
- Полиморфизм — это возможность работать с разными вариантами через единый интерфейс.
- В Rust это реализуется через enum + match.
Пример:
fn main() {
enum Shape {
Rectangle { width: f32, height: f32 },
Triangle { side: f32 },
Circle { radius: f32 },
}
impl Shape {
pub fn perimeter(&self) -> f32 {
match self {
Shape::Rectangle { width, height } => width * 2.0 + height * 2.0,
Shape::Triangle { side } => side * 3.0,
Shape::Circle { radius } => radius * 2.0 * std::f32::consts::PI
}
}
pub fn area(&self) -> f32 {
match self {
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { side } => side * 0.5 * 3.0_f32.sqrt() / 2.0 * side,
Shape::Circle { radius } => radius * radius * std::f32::consts::PI
}
}
}
fn print_area(shape: Shape) {
println!("{}", shape.area());
}
fn print_perimeters(shapes: Vec<Shape>) {
for shape in shapes.iter() {
println!("{}", shape.perimeter());
}
}
}
Как реализовать Traits + Generics полиморфизм?:
Generics (Обобщённые типы)
Что это?
Generics позволяют писать код, который работает с разными типами данных, не дублируя логику. Это как шаблон, который можно применить к любому типу.
Пример:
fn print_value<T>(value: T) {
println!("Значение: {:?}", value);
}
Здесь T — это обобщённый тип. Функция print_value может принимать и строку, и число, и любой другой тип.
Полиморфизм через Traits + Generics
Как это работает?
С помощью generics можно написать функцию, которая принимает любой тип, реализующий определённый трейт. Это и есть полиморфизм — один и тот же код работает с разными типами, если они поддерживают нужный интерфейс.
Пример:
fn say_hello<T: Greet>(item: T) {
println!("{}", item.greet());
}
let person = Person;
say_hello(person); // Выведет: Привет!
Здесь T: Greet означает, что T должен реализовать трейт Greet. Функция say_hello работает с любым типом, который умеет "приветствовать".
Итог
- Generics — шаблон для работы с разными типами.
- Traits — интерфейс, который описывает, что тип может делать.
- Полиморфизм — возможность использовать один и тот же код для разных типов, если они реализуют нужный трейт.
Пример:
fn main() {
trait Shape {
fn perimeter(&self) -> f32;
fn area(&self) -> f32;
}
struct Rectangle { pub width: f32, pub height: f32 }
struct Triangle { pub side: f32 }
struct Circle { pub radius: f32 }
impl Shape for Rectangle {
fn perimeter(&self) -> f32 {
self.width * 2.0 + self.height * 2.0
}
fn area(&self) -> f32 {
self.width * self.height
}
}
impl Shape for Triangle {
fn perimeter(&self) -> f32 {
self.side * 3.0
}
fn area(&self) -> f32 {
self.side * 0.5 * 3.0_f32.sqrt() / 2.0 * self.side
}
}
impl Shape for Circle {
fn perimeter(&self) -> f32 {
self.radius * 2.0 * std::f32::consts::PI
}
fn area(&self) -> f32 {
self.radius * self.radius * std::f32::consts::PI
}
}
fn print_area<S: Shape>(shape: S) {
println!("{}", shape.area());
}
fn print_perimeters<S: Shape>(shapes: Vec<S>) { // !
for shape in shapes.iter() {
println!("{}", shape.perimeter());
}
}
}
Как реализовать Traits + статический полиморфизм?:
Пример:
fn say_hello<T: Greet>(item: T) {
println!("{}", item.greet());
}
fn main() {
let person = Person;
let cat = Cat;
say_hello(person); // Выведет: Привет!
say_hello(cat); // Выведет: Мяу!
}
Здесь say_hello — обобщённая функция, которая работает с любым типом, реализующим трейт Greet.
Как это работает вместе?
- Трейты описывают, что должен уметь тип.
- Статический полиморфизм позволяет писать код, который работает с любым типом, реализующим нужный трейт, без потери производительности.
Итог:
- Трейты — это "что должен уметь тип".
- Статический полиморфизм — это "как использовать разные типы в одном коде, не теряя скорости".
Пример:
trait Shape {
fn perimeter(&self) -> f32;
fn area(&self) -> f32;
}
struct Rectangle { pub width: f32, pub height: f32 }
struct Triangle { pub side: f32 }
struct Circle { pub radius: f32 }
impl Shape for Rectangle {
fn perimeter(&self) -> f32 {
self.width * 2.0 + self.height * 2.0
}
fn area(&self) -> f32 {
self.width * self.height
}
}
impl Shape for Triangle {
fn perimeter(&self) -> f32 {
self.side * 3.0
}
fn area(&self) -> f32 {
self.side * 0.5 * 3.0_f32.sqrt() / 2.0 * self.side
}
}
impl Shape for Circle {
fn perimeter(&self) -> f32 {
self.radius * 2.0 * std::f32::consts::PI
}
fn area(&self) -> f32 {
self.radius * self.radius * std::f32::consts::PI
}
}
Как реализовать Traits + динамический полиморфизм?:
Пример:
fn animal_sound(animal: &dyn SoundMaker) {
animal.make_sound();
}
fn main() {
let dog = Dog;
let cat = Cat;
animal_sound(&dog); // Гав!
animal_sound(&cat); // Мяу!
}
Здесь функция animal_sound принимает любой тип, который реализует SoundMaker, и вызывает его метод. Компилятор не знает заранее, что это будет — собака или кошка, но программа работает правильно.
Ключевые моменты:
- Трейты — это интерфейсы, которые описывают, что должен уметь тип.
- Динамический полиморфизм — это работа с разными типами через общий трейт, без привязки к конкретному типу.
- В Rust для динамического полиморфизма используются указатели на трейты (&dyn Trait или Box<dyn Trait>).
Пример:
fn main() {
trait Shape {
fn perimeter(&self) -> f32;
fn area(&self) -> f32;
}
struct Rectangle { pub width: f32, pub height: f32 }
struct Triangle { pub side: f32 }
struct Circle { pub radius: f32 }
impl Shape for Rectangle {
fn perimeter(&self) -> f32 {
self.width * 2.0 + self.height * 2.0
}
fn area(&self) -> f32 {
self.width * self.height
}
}
impl Shape for Triangle {
fn perimeter(&self) -> f32 {
self.side * 3.0
}
fn area(&self) -> f32 {
self.side * 0.5 * 3.0_f32.sqrt() / 2.0 * self.side
}
}
impl Shape for Circle {
fn perimeter(&self) -> f32 {
self.radius * 2.0 * std::f32::consts::PI
}
fn area(&self) -> f32 {
self.radius * self.radius * std::f32::consts::PI
}
}
fn print_area(shape: &dyn Shape) {
println!("{}", shape.area());
}
fn print_perimeters(shapes: Vec<&dyn Shape>) {
for shape in shapes.iter() {
println!("{}", shape.perimeter());
}
}
}
Как реализовать Enum + inner structs полиморфизм?:
Это структуры данных, которые хранятся внутри вариантов enum - { name: String, age: u8 } и { name: String, breed: String } — это внутренние структуры.
Они позволяют каждому варианту enum хранить свои уникальные данные.
Пример:
fn main() {
enum ShapeEnum {
Rectangle(Rectangle),
Triangle(Triangle),
Circle(Circle)
}
struct Rectangle { pub width: f32, pub height: f32 }
struct Triangle { pub side: f32 }
struct Circle { pub radius: f32 }
trait Shape {
fn perimeter(&self) -> f32;
fn area(&self) -> f32;
}
impl Shape for ShapeEnum {
fn perimeter(&self) -> f32 {
match self {
ShapeEnum::Rectangle(rect) => rect.perimeter(),
ShapeEnum::Triangle(tri) => tri.perimeter(),
ShapeEnum::Circle(circ) => circ.perimeter(),
}
}
fn area(&self) -> f32 {
match self {
ShapeEnum::Rectangle(rect) => rect.area(),
ShapeEnum::Triangle(tri) => tri.area(),
ShapeEnum::Circle(circ) => circ.area(),
}
}
}
impl Shape for Rectangle {
fn perimeter(&self) -> f32 {
self.width * 2.0 + self.height * 2.0
}
fn area(&self) -> f32 {
self.width * self.height
}
}
impl Shape for Triangle {
fn perimeter(&self) -> f32 {
self.side * 3.0
}
fn area(&self) -> f32 {
self.side * 0.5 * 3.0_f32.sqrt() / 2.0 * self.side
}
}
impl Shape for Circle {
fn perimeter(&self) -> f32 {
self.radius * 2.0 * std::f32::consts::PI
}
fn area(&self) -> f32 {
self.radius * self.radius * std::f32::consts::PI
}
} }
Способы мутабельности ОПП в Rust
- мутабельность можно использовать с полиморфизмом через методы трейтов с изменяемыми ссылками и обобщенные функции.
- механизмы мутабельности, такие как RefCell, поддерживают полиморфизм.
1. Мутабельные экземпляры структур
По умолчанию экземпляры структур в Rust неизменяемы. Чтобы разрешить изменение их полей, необходимо объявить экземпляр как мутабельный с помощью ключевого слова mut.
Пример:
struct Point {
x: i32,
y: i32,
}
let mut p = Point { x: 0, y: 0 };
p.x = 5; // Разрешено, так как p мутабельный
2. Методы с мутабельным self
В Rust можно определить трейты с методами, которые принимают &mut self, что позволяет изменять реализующие типы. Это форма полиморфизма, где разные типы обрабатываются единообразно и могут быть изменены. Например: Трейт MutableTrait может иметь метод mutate, который изменяет значение.
trait MutableTrait {
fn mutate(&mut self);
}
struct MyType {
value: i32,
}
impl MutableTrait for MyType {
fn mutate(&mut self) {
self.value += 1;
}
}
Для изменения состояния структуры через методы используется мутабельная ссылка на self (&mut self). Это позволяет методам изменять поля структуры, если экземпляр объявлен как мутабельный.
Пример:
struct Point {
x: i32,
y: i32,
}
impl Point {
fn move_by(&mut self, dx: i32, dy: i32) {
self.x += dx;
self.y += dy;
}
}
let mut p = Point { x: 0, y: 0 };
p.move_by(5, 3); // Изменяет состояние p
3. Внутренняя мутабельность
Rust предоставляет типы для внутренней мутабельности, такие как Cell и RefCell, которые позволяют изменять данные даже через общие (немытабельные) ссылки. Это полезно в ситуациях, где мутабельность нужна без явного объявления экземпляра как mut.
- Cell: Используется для типов, реализующих Copy. Позволяет заменять значение целиком.
- RefCell: Используется для любых типов, проверяет правила заимствования во время выполнения с помощью методов borrow_mut().
Пример сруктуры и трэйта с RefCell:
use std::cell::RefCell;
trait SomeTrait {
fn mutate(&self);
}
struct MyType {
value: RefCell<i32>,
}
impl SomeTrait for MyType {
fn mutate(&self) {
*self.value.borrow_mut() += 1;
}
}
Пример сруктуры с RefCell:
use std::cell::RefCell;
struct MutablePoint {
x: RefCell<i32>,
y: RefCell<i32>,
}
impl MutablePoint {
fn move_by(&self, dx: i32, dy: i32) {
*self.x.borrow_mut() += dx;
*self.y.borrow_mut() += dy;
}
}
let p = MutablePoint {
x: RefCell::new(0),
y: RefCell::new(0),
};
p.move_by(5, 3); // Изменяет состояние, хотя p не объявлен как mut
4. Мутабельные заимствования
Rust позволяет иметь либо несколько неизменяемых заимствований (&T), либо одно мутабельное заимствование (&mut T) объекта одновременно. Мутабельное заимствование предоставляет эксклюзивный доступ к объекту для изменения его состояния, что соответствует принципам безопасности Rust.
Пример:
struct Point {
x: i32,
y: i32,
}
let mut p = Point { x: 0, y: 0 };
let borrow1 = &mut p;
borrow1.x = 5;
// let borrow2 = &mut p; // Ошибка: два мутабельных заимствования недопустимы
5. Мутабельные коллекции
Мутабельные массивы, векторы и другие коллекции позволяют изменять их элементы, если сама коллекция объявлена как мутабельная с помощью mut.
Пример:
struct Point {
x: i32,
y: i32,
}
let mut points = [Point { x: 0, y: 0 }, Point { x: 1, y: 1 }];
points[0].x = 5; // Разрешено, так как points мутабельный
6.Обобщенные функции с изменяемыми ссылками
Обобщенные функции могут принимать изменяемые ссылки на типы, реализующие определенные трейты, обеспечивая полиморфное поведение с возможностью мутации. Например, функция process может работать с любым типом, реализующим SomeTrait, и изменять его.
trait SomeTrait {
fn do_something(&mut self);
}
fn process<T: SomeTrait>(item: &mut T) {
item.do_something();
}
Какие типы статического и динамического полиморфизма необходимо использовать для ML:
- Параметрический полиморфизм Generic Polymorphism(Основной тип):
позволяет создавать обобщенный код, который может работать с разными типами данных. В Rust он реализуется через механизмы generics и trait bounds.
Пример:
fn max<T: Shape>(a: T, b: T) -> T {
if a > b { a } else { b }
}
Этот тип полиморфизма позволяет функциям, структурам, перечислениям и методам работать с любыми типами данных. Используется через обобщения (generics). Например, можно определить функцию, которая работает с любым типом T:
fn print_value<T: std::fmt::Debug>(value: T) {
println!("{:?}", value);
}
Обобщения (generics) для универсальных структур данных с методами для обработки данных:
use std::marker::PhantomData;
use std::fmt::Debug;
struct Dataset<T, L> {
data: Vec<T>,
labels: Vec<L>,
_phantom: PhantomData<(T, L)>,
}
impl<T, L> Dataset<T, L>
where
T: Clone + Debug,
L: Clone + Debug,
{
fn new(data: Vec<T>, labels: Vec<L>) -> Self {
assert_eq!(data.len(), labels.len(), "Data and labels must have equal length");
Self {
data,
labels,
_phantom: PhantomData,
}
}
fn split(&self, ratio: f64) -> (Self, Self) {
let split_idx = (self.data.len() as f64 * ratio) as usize;
let (train_data, test_data) = self.data.split_at(split_idx);
let (train_labels, test_labels) = self.labels.split_at(split_idx);
(
Self::new(train_data.to_vec(), train_labels.to_vec()),
Self::new(test_data.to_vec(), test_labels.to_vec()),
)
}
fn transform<F, U>(&self, f: F) -> Dataset<U, L>
where
F: Fn(&T) -> U,
U: Clone + Debug,
{
let new_data = self.data.iter().map(f).collect();
Dataset::new(new_data, self.labels.clone())
}
fn shuffle(&mut self) {
use rand::seq::SliceRandom;
let mut indices: Vec<usize> = (0..self.data.len()).collect();
indices.shuffle(&mut rand::thread_rng());
self.data = indices.iter().map(|&i| self.data[i].clone()).collect();
self.labels = indices.iter().map(|&i| self.labels[i].clone()).collect();
}
- Ad-hoc полиморфизм (Основной тип):
позволяет реализовывать одну и ту же операцию для разных типов данных. В Rust он реализуется через trait-ы, которые позволяют определять общие методы для разных типов данных.
Пример:
trait Drawable {
fn draw(&self);
}
struct Circle {
radius: f64,
}
impl Drawable for Circle {
fn draw(&self) {
// Рисуем круг
}
}
struct Square {
side_length: f64,
}
impl Drawable for Square {
fn draw(&self) {
// Рисуем квадрат
}
}
fn draw_all(shapes: &[&dyn Drawable]) {
for shape in shapes {
shape.draw();
}
}
- Полиморфизм по наследованию или по включению (Subtype Polymorphism):
в Rust нет наследования в традиционном смысле, но с помощью механизма trait-ов можно реализовать похожую функциональность. Trait-ы могут наследоваться друг от друга, что позволяет создавать иерархию типов.
Пример:
trait Shape {
//определяем трейт Shape с методом area.
fn area(&self) -> f64;
}
struct Circle {
//определяем структуры Circle и Rectangle
radius: f64,
}
impl Shape for Circle {
//трейт Shape для структур с версиями area
fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
struct Rectangle {
width: f64,
height: f64,
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
fn main() {
//создаем вектор переменных типа &dyn Shape
let shapes: Vec<&dyn Shape> = vec![
&Circle { radius: 1.0 },
&Rectangle { width: 2.0, height: 3.0 },
];
for shape in shapes {
//вызываем метод area для каждой фигуры
println!("Area: {}", shape.area());
}
}
- Полиморфизм через трейты (Trait-based Polymorphism)
похож на интерфейсы в других языках программирования. Типы, которые реализуют определенный трейт, могут быть использованы в контексте, где требуется этот трейт. Это также называется полиморфизмом подтипов (Subtype Polymorphism).
trait Draw {
fn draw(&self);
}
struct Circle;
impl Draw for Circle {
fn draw(&self)
{ println!("Drawing a circle.");
} }
Трейты для определения интерфейсов моделей ML с обобщенными параметрами и ассоциированными типами для гибкости:
use std::fmt::Debug;
use std::fs::File;
use std::io::{Read, Write};
trait Model<D, L, P> {
type Error: Debug;
fn fit(&mut self, data: &D, labels: &L) -> Result<(), Self::Error>;
fn predict(&self, data: &D) -> Result<P, Self::Error>;
fn score(&self, data: &D, labels: &L) -> Result<f64, Self::Error>;
fn save(&self, path: &str) -> Result<(), Self::Error>;
fn load(&mut self, path: &str) -> Result<(), Self::Error>;
}
struct LogisticRegression {
weights: Vec<f64>,
bias: f64,
}
#[derive(Debug)]
struct ModelError(String);
impl<D, L, P> Model<D, L, P> for LogisticRegression
where
D: AsRef<[Vec<f64>]>,
L: AsRef<[f64]>,
P: From<Vec<f64>>,
{
type Error = ModelError;
fn fit(&mut self, data: &D, labels: &L) -> Result<(), Self::Error> {
let data = data.as_ref();
let labels = labels.as_ref();
if data.len() != labels.len() {
return Err(ModelError("Data and labels length mismatch".into()));
}
// Упрощенная реализация обучения логистической регрессии
self.weights.resize(data[0].len(), 0.1);
self.bias = 0.0;
Ok(())
}
fn predict(&self, data: &D) -> Result<P, Self::Error> {
let data = data.as_ref();
let mut predictions = Vec::with_capacity(data.len());
for sample in data {
let logit = sample.iter().zip(&self.weights).map(|(x, w)| x * w).sum::<f64>() + self.bias;
predictions.push((1.0 / (1.0 + (-logit).exp())).clamp(0.0, 1.0));
}
Ok(predictions.into())
}
fn score(&self, data: &D, labels: &L) -> Result<f64, Self::Error> {
let predictions = self.predict(data)?;
let labels = labels.as_ref();
let predictions: Vec<f64> = predictions.into();
let accuracy = predictions.iter().zip(labels).filter(|(p, l)| (p - l).abs() < 0.5).count() as f64 / labels.len() as f64;
Ok(accuracy)
}
fn save(&self, path: &str) -> Result<(), Self::Error> {
let mut file = File::create(path).map_err(|e| ModelError(e.to_string()))?;
let data = format!("{:?}\n{}", self.weights, self.bias);
file.write_all(data.as_bytes()).map_err(|e| ModelError(e.to_string()))?;
Ok(())
}
fn load(&mut self, path: &str) -> Result<(), Self::Error> {
let mut file = File::open(path).map_err(|e| ModelError(e.to_string()))?;
let mut contents = String::new();
file.read_to_string(&mut contents).map_err(|e| ModelError(e.to_string()))?;
let mut lines = contents.lines();
self.weights = lines.next().ok_or(ModelError("Missing weights".into()))?.replace("[", "").replace("]", "").split(", ").map(|x| x.parse().unwrap()).collect();
self.bias = lines.next().ok_or(ModelError("Missing bias".into()))?.parse().map_err(|e| ModelError(e.to_string()))?;
Ok(())
}
}
- Структурный полиморфизм:
в Rust может быть достигнут с помощью кортежных структур и их автоматической реализации trait-ов.
Пример:
#[derive(Debug)]
struct Point(i32, i32);
//определяем кортежную структуру Point
#[derive(Debug)]
struct Circle(Point, i32);
//определяем кортежную структуру Circle
fn main() {
//создаем переменные типа Point и Circle
let point = Point(1, 2);
let circle = Circle(point, 5);
println!("{:?}", point);
println!("{:?}", circle);
}
- Полиморфизм через макросы (Macro Polymorphism)
позволяет создавать макросы, которые могут принимать разные типы и количество аргументов, обеспечивая полиморфизм на уровне макросов.
Пример:
macro_rules! sum {
( $($x:expr),* ) => {
{
let mut result = 0;
$(
result += $x;
)*
result
}
};
}
fn main() {
let sum_int = sum!(1, 2, 3, 4, 5); // с целыми числами
let sum_float = sum!(1.1, 2.2, 3.3); // с числами с плавающей точкой
println!("Sum of integers: {}", sum_int);
println!("Sum of floats: {}", sum_float);
}
- Перечисления (enums)
для представления вариантов алгоритмов или гиперпараметров с динамической диспетчеризацией через Box<dyn OptimizerTrait>:
enum Optimizer {
SGD { learning_rate: f64, momentum: f64 },
Adam { learning_rate: f64, beta1: f64, beta2: f64, epsilon: f64 },
}
trait OptimizerTrait {
fn update(&self, params: &mut [f64], grads: &[f64]);
}
struct SGD {
learning_rate: f64,
momentum: f64,
velocity: Vec<f64>,
}
impl SGD {
fn new(learning_rate: f64, momentum: f64) -> Self {
Self {
learning_rate,
momentum,
velocity: Vec::new(),
}
}
}
impl OptimizerTrait for SGD {
fn update(&self, params: &mut [f64], grads: &[f64]) {
if self.velocity.is_empty() {
self.velocity.extend(vec![0.0; params.len()]);
}
for (v, (p, g)) in self.velocity.iter_mut().zip(params.iter_mut().zip(grads)) {
*v = self.momentum * *v - self.learning_rate * g;
*p += *v;
}
}
}
struct Adam {
learning_rate: f64,
beta1: f64,
beta2: f64,
epsilon: f64,
m: Vec<f64>,
v: Vec<f64>,
t: usize,
}
impl Adam {
fn new(learning_rate: f64, beta1: f64, beta2: f64, epsilon: f64) -> Self {
Self {
learning_rate,
beta1,
beta2,
epsilon,
m: Vec::new(),
v: Vec::new(),
t: 0,
}
}
}
impl OptimizerTrait for Adam {
fn update(&self, params: &mut [f64], grads: &[f64]) {
if self.m.is_empty() {
self.m.extend(vec![0.0; params.len()]);
self.v.extend(vec![0.0; params.len()]);
}
self.t += 1;
for (p, (m, (v, g))) in params.iter_mut().zip(self.m.iter_mut().zip(self.v.iter_mut().zip(grads))) {
*m = self.beta1 * *m + (1.0 - self.beta1) * g;
*v = self.beta2 * *v + (1.0 - self.beta2) * g * g;
let m_hat = *m / (1.0 - self.beta1.powi(self.t as i32));
let v_hat = *v / (1.0 - self.beta2.powi(self.t as i32));
*p -= self.learning_rate * m_hat / (v_hat.sqrt() + self.epsilon);
}
}
}
impl Optimizer {
fn create(&self) -> Box<dyn OptimizerTrait> {
match self {
Optimizer::SGD { learning_rate, momentum } => Box::new(SGD::new(*learning_rate, *momentum)),
Optimizer::Adam { learning_rate, beta1, beta2, epsilon } => Box::new(Adam::new(*learning_rate, *beta1, *beta2, *epsilon)),
}
}
}
- Полиморфизм через оператор Deref
поддерживает автоматическое разыменование типов данных с помощью оператора Deref. Это позволяет работать с объектами, как если бы они были другими типами данных.
Пример:
use std::ops::Deref;
struct MyString {
content: String,
}
impl Deref for MyString {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.content
}
}
fn main() {
let my_string = MyString {
content: String::from("Hello, Rust!"),
};
// С использованием Deref, можно работать с my_string как со String
println!("Length of my_string: {}", my_string.len());
println!("Uppercase: {}", my_string.to_uppercase());
}
-Принудительный полиморфизм (Coercion Polymorphism):
Этот тип полиморфизма возникает, когда один тип данных неявно приводится к другому типу. Например, ссылки на структуры могут быть неявно приведены к ссылкам на типы, которые они реализуют через трейты:
fn draw_shape(shape: &dyn Draw) {
shape.draw();
}
let circle = Circle;
draw_shape(&circle);
// Здесь происходит приведение типа &Circle к &dyn Draw
Полиморфизм через FFI и интеграцию с Python (PyTorch, TensorFlow)
Используется через внешние библиотеки.
use tch::{Tensor, nn, Device};
fn predict_torch(input: &[f32]) -> f32 {
let tensor = Tensor::of_slice(input);
tensor.sum(Kind::Float).into()
}
fn main() {
let input = [1.0, 2.0, 3.0];
println!("Torch Prediction: {}", predict_torch(&input));
}
Полиморфизм через ассоциированные типы (Associated Types)
Используется в ML, если необходимо сделать алгоритм независимым от типа модели.
trait Train {
type Output;
fn train(&self, data: &[f32]) -> Self::Output;
}
struct DecisionTree;
impl Train for DecisionTree {
type Output = Vec<f32>;
fn train(&self, data: &[f32]) -> Self::Output {
data.iter().map(|x| x * 2.0).collect()
}
}
fn main() {
let tree = DecisionTree;
let result = tree.train(&[1.0, 2.0, 3.0]);
println!("Trained model output: {:?}", result);
}
Обобщенные алгоритмы обучения
trait Model {
fn fit(&mut self, data: &[Vec<f64>], labels: &[f64]);
fn predict(&self, data: &[Vec<f64>]) -> Vec<f64>;
}
trait Dataset {
fn get_data(&self) -> &[Vec<f64>];
fn get_labels(&self) -> &[f64];
}
struct SimpleDataset {
data: Vec<Vec<f64>>,
labels: Vec<f64>,
}
impl Dataset for SimpleDataset {
fn get_data(&self) -> &[Vec<f64>] {
&self.data
}
fn get_labels(&self) -> &[f64] {
&self.labels
}
}
struct GradientBoosting;
impl Model for GradientBoosting {
fn fit(&mut self, data: &[Vec<f64>], labels: &[f64]) {
// Упрощенная реализация градиентного бустинга
}
fn predict(&self, data: &[Vec<f64>]) -> Vec<f64> {
vec![0.0; data.len()]
}
}
fn train<M, D>(model: &mut M, dataset: &D)
where
M: Model,
D: Dataset,
{
let data = dataset.get_data();
let labels = dataset.get_labels();
model.fit(data, labels);
}