trait-gen

crate documentation build status crate


* trait-gen * The trait_gen attribute * Examples * Alternative format * Code awareness issues * Limitations * Compatibility * Releases * License


This library provides attributes to generate the trait implementations for several types, without the need for custom declarative macros.

In the example below, the Add trait is implemented for Meter, Foot and Mile. The T type identifier is used to mark where the substitution takes place; it can be an existing type or an alias but it's not necessary.

```rust use traitgen::traitgen;

pub struct Meter(f64); pub struct Foot(f64); pub struct Mile(f64);

[trait_gen(T -> Meter, Foot, Mile)]

impl Add for T { type Output = T;

fn add(self, rhs: T) -> Self::Output {
    Self(self.0 + rhs.0)
}

} ```

The trait_gen attribute has the same effect as this custom declarative macro:

```rust macrorules! impladd_length { ($($t:ty)*) => ( $(impl Add for $t { type Output = $t;

        fn add(self, rhs: $t) -> Self::Output {
            Self(self.0 + rhs.0)
        }
    })*
)

}

impladdlength! { Meter Foot Mile } ```

The advantages of the first method are the clarity of the native code, the support of refactoring tools, editor syntactic and semantic awareness, and not having to convert the code into a declarative macro. Looking for the definition of an implementation method is also much easier with the full support of code-aware editors!

The disadvantage is the current lack of support for procedural macros with the IntelliJ plugin, although this is an ongoing work (see tracking issue). A few work-arounds are discussed later.

There are also a few limitations of the current version described in the Limitations section.

The trait_gen attribute

```rust

[trait_gen(T -> type1, type2, type3)]

impl Trait for T { // ... } ```

This attribute successively substitutes the T type parameter, which is used as a type in the attached source code, with each of the following types (type1, type2, type3) to generate all the implementations.

All paths beginning with T in the code have this segment replaced. For example, T::default() generates type1::default(), type2::default() and so on, but super::T is unchanged.

The code must of course be compatible with all the types, or the compiler will trigger the relevant errors. For example #[trait_gen(T -> u64, f64)] cannot be used with Self(0) because 0 is not a valid floating-point literal.

Any occurrence of the parameter with the ${T} format in doc comments, macros and string literals are replaced by the actual type in each implementation.

Examples

Here are a few examples of the substitutions that are supported.

```rust

[trait_gen(U -> u32, i32)]

impl AddMod for U { fn add_mod(self, other: U, m: U) -> U { const U: U = 0; let zero = U::default(); let offset: super::U = super::U(0); (self + other + U + zero + offset.0 as U) % m } } ```

This code, with struct Meter(f64), struct Foot(f64) and so on:

```rust

[trait_gen(T -> Meter, Foot, Mile)]

impl Add for T { type Output = T;

fn add(self, rhs: T) -> Self::Output {
    const T: f64 = 0.0;
    T(self.0 + rhs.0 + T)
}

} ```

This example shows the use of type arguments in generic traits:

```rust struct Meter(U); struct Foot(U);

trait GetLength { fn length(&self) -> T; }

[trait_gen(U -> f32, f64)]

impl GetLength for Meter { fn length(&self) -> U { self.0 as U } } ```

This attribute can be combined with another one to create a cross-product generator, implementing the trait for Meter<f32>, Meter<f64, Foot<f32>, Foot<f64>:

```rust

[trait_gen(T -> Meter, Foot)]

[trait_gen(U -> f32, f64)]

impl GetLength for T { fn length(&self) -> U { self.0 as U } } ```

Multisegment paths (paths with ::) and path arguments (<f32>) can be used in the parameters. Here for example, inner::U is used to avoid any confusion with types if many single-letter types have already been defined. Also, the types Meter and Foot keep a part of their original module path (units):

Note: inner needn't actually exist since it's replaced.

```rust

[trait_gen(inner::U -> units::Meter, units::Foot)]

impl Add for inner::U { type Output = inner::U;

fn add(self, rhs: Self) -> Self::Output {
    inner::U(self.0 + rhs.0)
}

} ```

The documentation can be customized in each implementation by using ${T}. This also works in macros and string literals:

```rust

use traitgen::traitgen;

trait Trait { fn text(&self) -> String; }

[trait_gen(T -> u32, u64)]

impl Trait for T { /// Produces a string representation for ${T} fn text(&self) -> String { call("${T}"); format!("${T}: {}", self) } }

asserteq!(1u32.text(), "u32: 1"); asserteq!(2u64.text(), "u64: 2"); ```

Alternative format

The attribute supports a shorter "legacy" format which was used in the earlier versions:

```rust

[trait_gen(type1, type2, type3)]

impl Trait for type1 { // ... } ```

Here, type2 and type3 are literally substituted for type1 to generate their implementation, then the original code is implemented for type1. This is a shortcut for the equivalent attribute with the other format:

```rust

[trait_gen(type1 -> type2, type3, type1)]

impl Trait for type1 { // ... } ```

Remark: this strange ordering comes from an optimization in the legacy format. This makes no difference, except the order of the compiler messages if there are warnings or errors in the code - that's the only reason we mention it here.

The short format can be used when there is no risk of confusion, like in the example below. All the Meter instances must change, it is unlikely to be mixed with Foot and Mile, so using an alias is unnecessary. The type to replace in the code must be in first position in the parameter list:

```rust use std::ops::Add; use traitgen::traitgen;

pub struct Meter(f64); pub struct Foot(f64); pub struct Mile(f64);

[trait_gen(Meter, Foot, Mile)]

impl Add for Meter { type Output = Meter;

fn add(self, rhs: Meter) -> Self::Output {
    Self(self.0 + rhs.0)
}

} ```

In some situations, one of the implemented types happens to be required in all the implementations. Consider the following example, in which the return type is always u64:

```rust pub trait ToU64 { fn into_u64(self) -> u64; }

[trait_gen(u64, i64, u32, i32, u16, i16, u8, i8)]

impl ToU64 for u64 { fn into_u64(self) -> u64 { // ERROR! Replaced by -> i64, u32, ... self as u64 } } ```

This doesn't work because the return type of into_u64() will be replaced too! To prevent it, an alias must be used (or the other attribute format). This works:

```rust type T = u64;

[trait_gen(T, i64, u32, i32, u16, i16, u8, i8)]

impl ToU64 for T { fn into_u64(self) -> u64 { self as u64 } } ```

That is how the other format came to be, by getting rid of the type alias and allowing a "local" type parameter:

```rust

[trait_gen(T -> u64, i64, u32, i32, u16, i16, u8, i8)]

fn ToU64 for T { /* ... */ } ```

Code awareness issues

rust-analyzer supports procedural macros for code awareness, so everything should be fine for editors based on this Language Server Protocol implementation. Unfortunately this isn't the case of all IDEs yet, which removes some benefits of using this macro. For instance, the IntelliJ plugin is not able to provide much support while typing the code for an unknown T type, nor can it find the definition of the implemented methods, or even suggest them.

Here are two work-arounds that help when typing the trait implementation. However, they can't do much about code awareness when the trait methods are used later. Hopefully the remaining IDEs will provide more support for procedural macros soon.

  • Defining a type alias for the identifier used in the attribute doesn't change the produced code, but it allows the editor to understand it without expanding the macro:

    ```rust pub trait ToU64 { fn into_u64(self) -> u64; }

    type T = u64;

    [trait_gen(T -> u64, i64, u32, i32, u16, i16, u8, i8)]

    impl ToU64 for T { fn into_u64(self) -> u64 { self as u64 } } ```

  • Implementing for an existing type then using it as the type parameter is another possibility, but it may look more confusing so use it with caution:

    ```rust pub trait AddMod { fn add_mod(self, other: Self, m: Self) -> Self; }

    // No need to use type T = u32 in such a simple case:

    [trait_gen(u32 -> u32, i32, u64, i64, f32, f64)]

    impl AddMod for u32 { fn add_mod(self, other: Self, m: Self) -> Self { (self + other) % m } } ```

Limitations

  • Rust doesn't allow alias constructors with the "legacy" format. Self or a trait associated type is usually equivalent - here, Self(1.0). If there is no alternative, consider using the Default trait or creating a specific one.

    ```rust type T = Meter;

    [trait_gen(T, Foot, Mile)]

    impl Neutral for T { fn mul_neutral(&self) -> Self { T(1.0) // <== ERROR, use Self(1.0) instead } } ```

    The -> attribute format allows the substitution:

    ```rust

    [trait_gen(T -> Meter, Foot, Mile)]

    impl Neutral for T { fn mul_neutral(&self) -> Self { T(1.0) // <== replaced } } ```

  • The procedural macro behind the attribute can't handle scopes, so it doesn't support any type declaration with the same type name as the attribute parameter, including generics. This, for instance, fails to compile:

    ```rust use num::Num; use traitgen::traitgen;

    trait AddMod { type Output; fn add_mod(self, rhs: Self, modulo: Self) -> Self::Output; }

    [trait_gen(T -> u64, i64, u32, i32)]

    impl AddMod for T { type Output = T;

    fn addmod(self, rhs: Self, modulo: Self) -> Self::Output { fn intmod (a: T, m: T) -> T { // <== ERROR, conflicting 'T' a % m } int_mod(self + rhs, modulo) } } ```

Compatibility

The trait-gen crate is tested for rustc 1.67.1 and greater, on Windows 64-bit and Linux 64/32-bit platforms. There shouldn't be any problem with older versions.

Releases

RELEASES.md keeps a log of all the releases.

License

Licensed under MIT license.