Абстракции Rust отличаются от привычных в ООП.
В частности, вместо классов (классов объектов) используются классы типов, которые называются «trait» (не путать с трейтом из Scala, где под этим термином скрывается примеси - миксин).
Классы типов не являются уникальными для Rust, они поддерживаются в Haskell, Mercury, Go и могут быть реализованы.
в слегка извращенном виде в Scala и C++.
Я хочу на примере показать, как они реализованы в Rust. двойные числа и проанализировать отдельные нетривиальные (или слабо проработанные) моменты.
Интерфейсы числовых типов довольно громоздки, и я буду вставлять сюда только фрагменты кода.
Весь код доступен по адресу github (Обновление: рабочая версия доступна на crates.io ).
Большинство реализованных здесь интерфейсов находятся в экспериментальном или нестабильном состоянии и могут быть изменены.
Я постараюсь поддерживать код и текст в актуальном состоянии.
Rust поддерживает перегрузку операторов, но в отличие от C++ операторы имеют синонимичный метод с общим литеральным именем.
Так а+б можно написать а.
добавить(б) и чтобы переопределить операцию «+», вам просто нужно реализовать метод add. Что такое класс типа? Класс типа часто сравнивают с интерфейсом.
Действительно, он определяет, что можно делать с определенными типами данных, но эти операции должны реализовываться отдельно.
В отличие от интерфейса, реализация класса типа для определенного типа не создает новый тип, а живет со старым, хотя старый тип может ничего не знать о реализуемом интерфейсе.
Чтобы код, использующий этот интерфейс, работал с этим типом данных, не нужно редактировать ни тип данных, ни интерфейс, ни код — достаточно реализовать интерфейс для типа.
В отличие от интерфейса в стиле ООП, класс типов может ссылаться на тип несколько раз.
В Rust эта ссылка называется Себя , в Haskell это можно назвать практически как угодно.
Например, в Haskell метод «+» требует, чтобы оба аргумента были одного и того же типа, и ожидает возврата объекта точно такого же типа (в Rust в классе типов Добавлять эти типы могут быть разными — в частности, вы можете добавить Duration и Timespec).
Тип возвращаемого значения также важен — аргументы могут вообще не использовать тип из класса, и компилятор решает, какую реализацию метода использовать, исходя из того, какой тип следует получить.
Например, в Rust есть класс типов Нуль и код
будет присваивать разные нули переменным разных типов.let float_zero:f32 = Zero::zero(); let int_zero:i32 = Zero::zero();
Как это делается в Rust
Описание
Класс типа создается с помощью ключевого слова типажа, за которым следует имя (возможно, с параметрами, как в C++) и список методов.Метод может иметь реализацию по умолчанию, но такая реализация не имеет доступа к внутренностям типа и должна использовать другие методы (например, выражение неравенства ( != , пе ) через отрицание равенства).
pub trait PartialEq {
/// This method tests for `self` and `other` values to be equal, and is used by `==`.
fn eq(&self, other: &Self) -> bool; /// This method tests for `!=`.
#[inline]
fn ne(&self, other: &Self) -> bool { !self.eq(other) }
}
Вот описание класса типов из стандартной библиотеки, включающего типы, допускающие сравнение на равенство.
Первый аргумент, называемый себя или &себя каждый метод аналогичен этот из классического ООП.
Наличие амперсанда указывает на способ передачи владения объектом и, в отличие от C++, не влияет на возможность его изменения (передача по ссылке или по значению).
Право на изменение объекта дается явным указанием mut. Второй аргумент должен быть того же типа, что и первый – на это указывает Себя .
Позже мы столкнемся с тем, что этот аргумент не обязателен — в итоге мы получим что-то вроде статических методов, хотя по сути они всё равно остаются «динамическими» — диспетчеризация осуществляется по другим параметрам или типу ожидаемого результата.
pub trait Add<RHS,Result> {
/// The method for the `+` operator
fn add(&self, rhs: &RHS) -> Result;
}
Операция «+» в Rust не обязательно требует одного и того же типа аргументов и результатов.
Для этого класс типов сделан шаблонным: аргументами шаблона являются типы второго аргумента и результата.
Для сравнения, в Haskell классы типов не параметризуются (кроме самого типа), но могут содержать не отдельные типы, а пары, тройки и другие наборы типов (расширение MultiParamTypeClasses), что позволяет делать подобные вещи.
Rust обещает добавить поддержку этой функции к каждому выпуску.
Стоит обратить внимание на синтаксическое отличие от C++ — описание сущности в Rust (в данном случае класса типа) само по себе является шаблоном, тогда как в C++ шаблон объявляется отдельно с помощью ключевого слова.
Подход C++ в некотором смысле более логичен, но более сложен для понимания.
Давайте посмотрим на другой пример Нуль : pub trait Zero: Add<Self, Self> {
/// Returns the additive identity element of `Self`, `0`.
/// /// # Laws /// /// ```{.
text}
/// a + 0 = a ∀ a ∈ Self
/// 0 + a = a ∀ a ∈ Self
/// ```
///
/// # Purity
///
/// This function should return the same result at all times regardless of
/// external mutable state, for example values stored in TLS or in
/// `static mut`s.
// FIXME (#5527): This should be an associated constant
fn zero() -> Self;
/// Returns `true` if `self` is equal to the additive identity.
#[inline]
fn is_zero(&self) -> bool;
}
В описании этого типа класса можно увидеть наследование — для реализации Zero необходимо сначала реализовать Add (параметризованный тем же типом).
Это обычное наследование интерфейсов без реализации.
Также допускается множественное наследование; для этого предки перечисляются через «+».
Обратите внимание на метод fn ноль() -> Я; .
Это можно считать статическим методом, хотя позже мы увидим, что он несколько более динамичен, чем статические методы в ООП (в частности, их можно использовать для реализации «фабрик»).
Выполнение
Рассмотрим реализацию Add для комплексных чисел: impl<T: Clone + Num> Add<Complex<T>, Complex<T>> for Complex<T> {
#[inline]
fn add(&self, other: &Complex<T>) -> Complex<T> {
Complex::new(self.re + other.re, self.im + other.im)
}
}
Комплексные числа — это обобщенный тип, который параметризуется представлением действительного числа.
Реализация сложения также параметризована — она применима к комплексным числам над различными вариантами действительных чисел, если для этих вещественных чисел реализован какой-то интерфейс.
В данном случае требуемый интерфейс слишком богат — предполагает наличие реализаций Клонировать (позволяя создать копию) и Число (содержащий основные операции над числами, в частности наследование Добавлять ).
Получение
Если вам лень писать реализации простых стандартных интерфейсов самостоятельно, эту рутинную работу можно передать компилятору с помощью директивы deriving. #[deriving(PartialEq, Clone, Hash)]
pub struct Complex<T> {
/// Real portion of the complex number
pub re: T,
/// Imaginary portion of the complex number
pub im: T
}
Здесь разработчикам библиотек предлагается создать реализацию интерфейсов PartialEq, Clone и Hash, если тип T поддерживает все, что им нужно.
В настоящее время поддерживается автоматическое создание реализаций для классов типов Clone, Hash, Encodable, Decodable, PartialEq, Eq, PartialOrd, Ord, Rand, Show, Zero, Default, FromPrimitive, Send, Sync и Copy. Классы числовых типовВ модуле станд::номер описано большое количество классов типов, связанных с различными свойствами чисел.
Они могут ссылаться на некоторые другие признаки — для операций сравнения и выделения памяти (например Копировать сообщает компилятору, что этот тип можно копировать побайтно).
Я выделил на диаграмме интерфейсы, которые я реализовал для двойных чисел.
Реализация двойственных чисел.
Тип данных тривиален: pub struct Dual<T> {
pub val:T,
pub der:T
}
В отличие от комплексных чисел в стандартной библиотеке, я постарался реализовать интерфейс с минимальными допущениями.
Таким образом, моя реализация Add требует только интерфейса Add исходного типа, а Mul требует только Mul+Add. Иногда это приводило к появлению странного кода.
Например, Signed не обязан поддерживать Clone, и чтобы положительное двойное число в методе abs возвращало свою копию, его нужно было прибавить к нулю.
impl<T:Signed> Signed for Dual<T> {
fn abs(&self) -> Dual<T> {
if self.is_positive() || self.is_zero() {
self+Zero::zero() // XXX: bad implementation for clone
} else if self.is_negative() {
-self
} else {
fail!("Near to zero")
}
}
}
В противном случае компилятор не сможет отслеживать право собственности на этот объект.
Обратите внимание, что тип Ноль::ноль() явно не указано.
Компилятор угадывает, каким оно должно быть, пытаясь добавить с помощью себя , который реализует Число , и следовательно, Добавлять .
Но тип Self на момент компиляции еще не известен — он задается параметром шаблона.
Что означает метод нуль динамически располагается в таблице методов реализации Число Для Двойной ! Еще отмечу интересный прием, как в Float реализуются целочисленные константы, характеризующие весь тип.
То есть они не могут получить на вход экземпляр (он может не существовать в нужном контексте), но должен быть аналогом статических методов.
Такая же проблема часто возникает в Haskell, и для ее решения в такие методы добавляют поддельный параметр нужного типа.
Haskell — ленивый язык, и его всегда можно передать как неиспользуемый аргумент. ошибка "Не используется" .
Говоря строгим языком Rust, этот прием не работает, и создание объекта для него может оказаться слишком дорогим.
По этой причине используется обходной путь – передача Никто тип Вариант #[allow(unused_variable)]
impl<T:Float> Float for Dual<T> {
fn mantissa_digits(_unused_self: Option<Dual<T>>) -> uint {
let n: Option<T> = None;
Float::mantissa_digits(n)
}
}
Поскольку опция не используется, компилятор по умолчанию выдает предупреждение.
Подавить его можно двумя способами — начав имя параметра с символа «_» или используя директиву #[allow(unused_variable)].
Теги: #Rust #двойные числа #классы типов #математика #Rust
-
Суперакция Lenovo Для Корпоративных Клиентов
19 Oct, 24 -
Обучение По Stm32 Для Масс
19 Oct, 24 -
Tower Defense (Портативный)
19 Oct, 24 -
Microsoft Поможет Найти Ошибки В Oss
19 Oct, 24 -
Дайджест Kolibrios №5: Мы Снова С Вами
19 Oct, 24