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 {
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> {
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> {
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> {
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()
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)].