Traits in Rust has a few syntax problems.
Most of them are related to impl blocks:
- Most traits have only one function to implement, when you want to implement only one function - writing at least 5 lines is a lot.
- Often there are several implementations of different traits in a row, but for one type. As a result, a large amount of code is repeated.
- Implementation of associated type takes 2 whole lines of code (for code cleanliness you should add Enter), this is too much for such a small thing, considering how it is usually used (return value type).
The third problem also applies to trait declarations, not just impl blocks.
Besides this, there is another, not so important problem, which we will not solve, but it is worth mentioning: the name of a trait often coincides with the name of its only function.
Any problems related to excessive code (in this case, all of them) hinder not only writing but also reading and refactoring the code.
Possible solutions
Only possible solutions are suggested here, you can write a comment if you find a better one.
The first two problems are better solved together, because solving the second can solve the first. I propose to add common blocks. First follow the words impl for
, then the type, for which the traits are implemented, and then the block.
impl for Point {
//...
}
Inside the block, you can write trait functions and associated types with a certain syntax: In place of the name usually follows the construction Trait::item, for example:
type Add::Output = Point;
fn Add::add(self, other: Point) -> Add::Output { //... }
This solution is inefficient when the trait is big, it's exactly for small traits.
A solution to the third problem might be to add automatic derivation of the associated type from function signatures and other associated types, implemented by the user.
An alternative solution may be the introduction of an undefined type in traits, with this syntax: As a type you write ?
, and when trait is implemented, this type is derived from the function signature itself, this type can only be used inside the function signature and for each function will be different, for example:
trait MyTrait {
fn foo(self) -> ?
}
impl MyTrait for MyStruct {
fn foo(self) -> i32 { 10 }
}
This solution has a few disadvantages:
- You can't explicitly prescribe the type
- You can't use a type in multiple function signatures
Example
If you combine the two approaches described above (in the case of the return value type, it does not matter which approach is taken), then the implementation of the addition and subtraction operators for Point
turns into:
use std::ops::{Add, Sub};
impl for Point {
fn Add::add(self, other: Point) -> Point {
Point::new(self.x + other.x, self.y + other.y)
}
fn Sub::sub(self, other: Point) -> Point {
Point::new(self.x - other.x, self.y - other.y)
}
}
Instead of the classic:
use std::ops::{Add, Sub};
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Self::Output {
Point::new(self.x + other.x, self.y + other.y)
}
}
impl Sub for Point {
type Output = Point;
fn sub(self, other: Point) -> Self::Output {
Point::new(self.x - other.x, self.y - other.y)
}
}
The more traits are implemented, the better these approaches show their effectiveness.