Найти тему
Один Rust не п...Rust

Полиморфизм в Rust

Оглавление

Для чего нужна данная статья? :
- Получить представление о полиморфизме в Rust.
- Написать код для задач машинного обучения.
- Найти компромиссы между разными видами и типами полиморфизма.

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

1. Для создания более гибких API - необходимы Вам и тем кто потребляет Ваш код.

2. Повторное использование кода - необходимо Вам и тем кто потребляет Ваш код.

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

4. Создать полноценную интеграцию с библиотеками ML, такими как ndarray, tch-rs (bindings для PyTorch), burn, linfa.

Какие виды полиморфизма в Rust? :

  • Статический
  • Динамический

Как реализован полиморфизм в 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 (по сути, являющего собой анонимные типовые параметры).
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.

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

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. Типы нужно передавать посредством указателя (два указателя - на значение и на динамическую таблицу), ссылки или умного указателя.
Запрещает использование дженериков.
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 полиморфизм?:

Пример:

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 полиморфизм?:

Пример:

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 + статический полиморфизм?:

Пример:

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 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 полиморфизм?:

Пример:

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

}

} }

Какие типы статического и динамического полиморфизма необходимо использовать для 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);

}