Классы Числовых Типов В Rust

Абстракции 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. Классы числовых типовВ модуле станд::номер описано большое количество классов типов, связанных с различными свойствами чисел.

Они могут ссылаться на некоторые другие признаки — для операций сравнения и выделения памяти (например Копировать сообщает компилятору, что этот тип можно копировать побайтно).

Я выделил на диаграмме интерфейсы, которые я реализовал для двойных чисел.



Классы Числовых Типов В Rust

Реализация двойственных чисел.

Тип данных тривиален: 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

Вместе с данным постом часто просматривают: