Для чего нужна данная статья? :
- Получить представление о полиморфизме в 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);
}