📜 ⬆️ ⬇️

Numeric Classes of Types in Rust

Abstractions Rust differ from the usual in the PLO. In particular, instead of classes (classes of objects), classes of types are used, which are called “trait” (should not be confused with trait from Scala, where this term hides impurities - mixin).
Type classes are not unique to Rust, they are supported in Haskell, Mercury, Go, and can be implemented in a slightly perverted way in Scala and C ++.

I want to show how they are implemented in Rust using the example of dual numbers and to parse individual nontrivial (or poorly worked out) moments.

Numeric type interfaces are rather cumbersome, and I will insert here only code snippets. All code is available on github (Update: a working version is available on crates.io ).
Most of the interfaces implemented here have the status of experemental or unstable and most likely will change. I will try to keep the code and text relevant.
')
Rust supports overloading operations, but, unlike C ++, operations have a synonym method with the usual letter name. So a + b can be written a.add (b) , and to override the '+' operation, you just need to implement the add method.


What is a type class?

Class types are often compared with the interface. Indeed, it determines what can be done with some data types, but these operations must be implemented separately. Unlike the interface, the implementation of a type class for some type does not create a new type, but lives with the old one, although the old type may not know anything about the implemented interface. In order for the code using this interface to work with this data type, neither the data type, nor the interface, nor the code need to be corrected - it is enough to implement an interface for the type.

Unlike an OOP-style interface, a type class can refer to a type several times. In Rust, this link is called Self , in Haskell, it can be called almost anything. For example, in Haskell, the '+' method requires that both arguments be of exactly one type and an object of exactly the same type is expected to return (in Rust, in the Add type class, these types can be different - in particular, Duration and Timespec can be added). The type of the return value is also important - the arguments may not use the type from the class at all, and which implementation of the method the compiler decides to use based on what type to get. For example, in Rust there is a class of types Zero and code
let float_zero:f32 = Zero::zero(); let int_zero:i32 = Zero::zero(); 
assign different zeros to variables of different types.

How it is made in Rust

Description

The type class is created by the trait keyword, followed by a name (possibly with parameters, like in C ++) and a list of methods. A method may have a default implementation, but such an implementation does not have access to the internals of the type and must use other methods (for example, express inequality ( ! = , Ne ) through the negation of equality).
 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) } } 
Here is a description of the type class from the standard library, which includes types that can be compared for equality.
The first argument, called self or & self of each method, is an analogue of this from classic OOP. The presence of an ampersend indicates the method of transferring ownership of the object and, unlike C ++, the ability to change it does not affect (passing by reference or by value). The right to modify the object gives an explicit indication of mut.
The second argument must be of the same type as the first - Self indicates this.
Later we will come across the fact that this argument is not obligatory - something turns out like static methods, although in fact they still remain “dynamic” - the distribution is carried out by other parameters or by the type of expected result.
 pub trait Add<RHS,Result> { /// The method for the `+` operator fn add(&self, rhs: &RHS) -> Result; } 
Operation '+' in Rust is not required to require the same types of arguments and results. For this, the type class is made template: template arguments are the types of the second argument and the result.
For comparison, in Haskell, type classes are not parameterized (except by the type itself), but can contain not separate types, but pairs, triples, and other types of types (extension MultiParamTypeClasses), which allows you to do similar things. To the release of Rust promise to add support for this feature.
It is worth paying attention to the syntactic difference from C ++ - the description of an entity in Rust (in this case, a type class) is itself a template, and in C ++ the template is declared separately using a keyword. The C ++ approach is somewhat more logical, but more difficult to understand.
Consider another Zero example:
 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; } 
In the description of this type class, inheritance can be seen - for the implementation of Zero, you must first implement Add (parameterized by the same type). This is the usual inheritance of interfaces without implementation. Multiple inheritance is allowed, for this ancestor is listed through '+'.
Pay attention to the fn zero () -> Self method ; . This can be considered as a static method, although we will see later that it is somewhat more dynamic than the static methods in OOP (in particular, they can be used to implement "factories").

Implementation

Consider the implementation of Add for complex numbers:
 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) } } 
Complex numbers are a generic type, parameterized by the representation of a real number. The implementation of addition is also parameterized - it is applicable to complex numbers over various valid options, if a certain interface is implemented for these valid ones. In this case, the required interface is too rich - it assumes the existence of implementations of Clone (which allows you to create a copy) and Num (containing basic operations on numbers, in particular, the inheritance Add ).

Deriving

If you are too lazy to write implementations of simple standard interfaces, this routine work can be passed to the compiler using the deriving directive.
 #[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 } 
Here, library developers are asked to create an implementation of the PartialEq, Clone and Hash interfaces, if type T supports everything that is necessary.
Currently, auto-generation implementations are supported for the types of Clone, Hash, Encodable, Decodable, PartialEq, Eq, PartialOrd, Ord, Rand, Show, Zero, Default, FromPrimitive, Send, Sync, and Copy types.

Numeric type classes

The std :: num module describes a large number of classes of types associated with different properties of numbers.
They can refer to some other traits - for comparison and memory allocation operations (for example, Copy prompts the compiler that this type can be copied byte-by-bye).
I highlighted the interfaces that I implemented for dual numbers in a diagram.

Implementation of dual numbers

The data type is trivial:
 pub struct Dual<T> { pub val:T, pub der:T } 

Unlike the complex numbers from the standard library, I tried to implement the interface based on minimal assumptions. So the Add implementation from me requires only the Add interface from the source type, and the Mul only requires Mul + Add.
Sometimes this led to strange code. For example, Signed is not required to support Clone, and to return a copy of it to a positive dual number in the abs method, we had to add it to zero
 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") } } } 
Otherwise, the compiler cannot trace ownership of this object.
Please note that the type Zero :: zero () is not explicitly set. The compiler guesses how it should be when attempting to add with self , which implements Num , and, consequently, Add <Self, Self> . But the Self type is not known at the time of compilation - it is set by the template parameter. So the zero method is dynamically located in the table of Num implementation methods for Dual <T> !

I would also note an interesting trick, as in Float integer constants are implemented, which characterize the entire type. That is, they cannot receive an instance at the entrance (it may not be in the right context), but should be an analogue of static methods. The same problem often arises in Haskell, and to solve it such methods add a fake parameter with the necessary type. Haskell has a lazy language and you can always pass the error "Not used" as an unused argument. In a strict Rust language, this technique does not work, and creating an object for this may be too expensive. By this, a workaround is used - transmitted None type Option <Self>
 #[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) } } 
Since the parameter is not used, by default the compiler issues a warning. It can be suppressed in two ways - by starting the name of the parameter with the character '_' or using the directive # [allow (unused_variable)].

Source: https://habr.com/ru/post/239151/


All Articles