Skip to content

Trait implementation generator macro

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT
Notifications You must be signed in to change notification settings

blueglyph/trait_gen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

crate documentation build status crate



The 'trait-gen' Crate

This library provides an attribute macro to generate the trait implementations for several types without needing custom declarative macros, code repetition, or blanket implementations. It makes the code easier to read and to maintain.

Here is a short example:

use trait_gen::trait_gen;

#[trait_gen(T -> u8, u16, u32, u64, u128)]
impl MyLog for T {
    fn my_log2(self) -> u32 {
        T::BITS - 1 - self.leading_zeros()
    }
}

The trait_gen attribute generates the following code by replacing T with the types given as arguments:

impl MyLog for u8 {
    fn my_log2(self) -> u32 {
        u8::BITS - 1 - self.leading_zeros()
    }
}
impl MyLog for u16 {
    fn my_log2(self) -> u32 {
        u16::BITS - 1 - self.leading_zeros()
    }
}
// and so on for the remaining types

Usage

The attribute is placed before the pseudo-generic implementation code. The generic argument is given first, followed by a right arrow (->) and a list of type arguments.

#[trait_gen(T -> Type1, Type2, Type3)]
impl Trait for T {
    // ...
}

The attribute macro successively substitutes the generic argument T in the code with the following types (Type1, Type2, Type3) to generate all the implementations.

All the type paths beginning with T in the code have this part replaced. For example, T::default() generates Type1::default(), Type2::default() and so on, but super::T is unchanged because it belongs to another scope.

The code must be compatible with all the types, or the compiler will trigger the relevant errors. For example, #[trait_gen(T -> u64, f64)] cannot be applied to let x: T = 0; because 0 is not a valid floating-point literal.

Finally, the actual type replaces any ${T} occurrence in doc comments, macros, and string literals.

Notes:

  • Using the letter "T" is not mandatory; any type path will do. For example, gen::Type is fine too. But to make it easy to read and similar to a generic implementation, short upper-case identifiers are preferred.
  • Two or more attributes can be chained to generate all the combinations.
  • trait_gen isn't restricted to trait implementations: it can be used on type implementations too.
  • type_gen is a synonym attribute that can be used instead of trait_gen when the type_gen feature is enabled (it requires use trait_gen::type_gen).

Motivation

There are several ways to generate multiple implementations:

  • copy them manually
  • use a declarative macro
  • use a blanket implementation

The example of implementation above could be achieved with this declarative macro:

macro_rules! impl_my_log {
    ($($t:ty)*) => (
        $(impl MyLog for $t {
            fn my_log2(self) -> u32 {
                $t::BITS - 1 - self.leading_zeros()
            }
        })*
    )
}

impl_my_log! { u8 u16 u32 u64 u128 }

But it's noisy and harder to read than native code. We must write a custom macro each time, with its declaration, pattern, and translation of a few elements like the parameters (here, $t). Moreover, IDEs can't often provide contextual help or apply refactoring in the macro code.

It's also quite annoying and unhelpful to get this result when we're looking for the definition of a method when it has been generated by a declarative macro:

impl_my_log! { u8 u16 u32 u64 u128 }

Using a blanket implementation has other drawbacks:

  • It forbids any other implementation except for types of the same crate that are not already under the blanket implementation, so it only works when the implementation can be written for all bound types, current and future.
  • Finding a trait that corresponds to what we need to write is not always possible. The num crate provides a lot of help for primitives, for instance, but not everything is covered.
  • Even when the operations and constants are covered by traits, it quickly requires a long list of trait bounds.

Writing the first example as a blanket implementation looks like this. Since it's a short example, there is only one bound, but instead of T::BITS we had to use a trick that isn't very good-looking:

use std::mem;
use num_traits::PrimInt;

impl<T: PrimInt> MyLog for T {
    fn my_log2(self) -> u32 {
        mem::size_of::<T>() as u32 * 8 - 1 - self.leading_zeros()
    }
}

Conditional Code

The use of conditional inclusion of code offers more flexibility in the implementation. Within a trait-gen implementation, the pseudo-attribute #[trait_gen_if(T in Type1, Type2, Type3] disables the attached code if T isn't in the list of types.

Here is an example:

# use trait_gen::trait_gen;

trait Binary {
    const DECIMAL_DIGITS: usize;
    const SIGN: bool = false;
    fn display_length() -> usize;
    fn try_neg(self) -> Option<Self> where Self: Sized { None }
}

#[trait_gen(T -> i8, u8, i32, u32)]
impl Binary for T {
    #[trait_gen_if(T in i8, u8)]
    const DECIMAL_DIGITS: usize = 3;
    #[trait_gen_if(T in i32, u32)]
    const DECIMAL_DIGITS: usize = 10;
    #[trait_gen_if(T in i8, i32)]
    const SIGN: bool = true;

    fn display_length() -> usize {
        Self::DECIMAL_DIGITS + if T::SIGN { 1 } else { 0 }
    }

    #[trait_gen_if(T in i8, i32)]
    fn try_neg(self) -> Option<Self> {
        Some(-self)
    }
}

We said it was a pseudo attribute because it's removed by trait-gen when it generates the final code that will be seen by the compiler. So trait_gen_if mustn't be declared.

We've seen earlier that type_gen was a synonym of trait_gen. For the sake of coherency, a type_gen_if is also provided as a synonym of trait_gen_if. Both type_gen and type_gen_if require the type_gen feature.

Thanks to Daniel Vigovszky for giving me this idea! He first implemented it, although differently, in a fork called conditional_trait_gen. I had pondered about some use-cases that would require such a feature in an old post but never got around to implementing it until now.

Examples

Here are a few examples of the substitutions that are supported; you'll find more in the integration tests of the library.

The first example is more an illustration of what is and isn't replaced than a practical implementation:

#[trait_gen(U -> u32, i32, u64, i64)]
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
    }
}

is expanded into (we only show the first type, u32):

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

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

struct Meter<U>(U);
struct Foot<U>(U);

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

#[trait_gen(U -> f32, f64)]
impl GetLength<U> for Meter<U> {
    fn length(&self) -> U {
        self.0 as U
    }
}

This attribute can be combined with another one to create a generic composition, implementing the trait for Meter<f32>, Meter<f64>, Foot<f32>, Foot<f64>:

#[trait_gen(T -> Meter, Foot)]
#[trait_gen(U -> f32, f64)]
impl GetLength<U> for T<U> {
    fn length(&self) -> U {
        self.0 as U
    }
}

is expanded into this:

  • impl GetLength<f32> for Meter<f32> {
        fn length(&self) -> f32 { self.0 as f32 }
    }
    impl GetLength<f64> for Meter<f64> {
        fn length(&self) -> f64 { self.0 as f64 }
    }
    impl GetLength<f32> for Foot<f32> {
        fn length(&self) -> f32 { self.0 as f32 }
    }
    impl GetLength<f64> for Foot<f64> {
        fn length(&self) -> f64 { self.0 as f64 }
    }

Multisegment paths (paths with ::) and path arguments (<f32>) can be used in the arguments. For example, gen::U is used to avoid any confusion with types if many single-letter types have already been defined.

Also, Meter and Foot must keep the units module path in the arguments because there wouldn't be a substitution if those paths were in the code (the type in impl Add for units::gen::U doesn't begin with gen::U and thus isn't replaced).

Note: gen needn't any declaration since it's replaced by the macro.

#[trait_gen(gen::U -> units::Meter<f32>, units::Foot<f32>)]
impl Add for gen::U {
    type Output = gen::U;

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

More complicated types can be used, like references or slices. This example generates implementations for the immutable, mutable and boxed referenced types:

#[trait_gen(T -> u8, u16, u32, u64, u128)]
impl MyLog for T {
    fn my_log2(self) -> u32 {
        T::BITS - 1 - self.leading_zeros()
    }
}

#[trait_gen(T -> u8, u16, u32, u64, u128)]
#[trait_gen(U -> &T, &mut T, Box<T>)]
impl MyLog for U {
    fn my_log2(self) -> u32 {
        MyLog::my_log2(*self)
    }
}

As you see in the generic composition, the first generic argument U can be used in the second attribute argument list (the order of the attributes doesn't matter).

Finally, this example shows how the documentation and string literals can be customized in each implementation by using the ${T} format:

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

#[trait_gen(T -> u32, i32, u64, i64)]
impl Repr for T {
    /// Produces a string representation for `${T}`
    fn text(&self) -> String {
        call("${T}");
        format!("${T}: {}", self)
    }
}

assert_eq!(1_u32.text(), "u32: 1");
assert_eq!(2_u64.text(), "u64: 2");
  • impl Repr for u32 {
        /// Produces a string representation for `u32`
        fn text(&self) -> String {
            call("u32");
            format!("u32: {}", self)
        }
    }
    // ...

Note: there is no escape code to avoid the substitution; if you need ${T} for another purpose and you don't want it to be replaced, you must use concat! in a doc attribute to split the pattern; for example #[doc = concat!("my ${", "T} variable")]. Or you must choose another generic argument; for example, U or my::T.

Legacy Format

The attribute used a shorter format in earlier versions, which is still supported even though it may be more confusing to read:

#[trait_gen(Type1, Type2, Type3)]
impl Trait for Type1 {
    // ...
}

Here, the code is generated as is for Type1, and then Type2 and Type3 are substituted for Type1 to generate their implementation. This is a shortcut for the equivalent attribute with the other format:

#[trait_gen(Type1 -> Type1, Type2, Type3)]
impl Trait for Type1 {
    // ...
}

The legacy format can be used when there is no risk of collision, like in the example below. All the Meter types must change, and it is unlikely to be mixed up with Foot and Mile. The type to replace in the code must be the first in the argument list:

use std::ops::Add;
use trait_gen::trait_gen;

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)
    }
}

Be careful not to replace a type that must remain the same in all implementations! Consider the following example, in which the return type is always u64:

pub trait ToU64 {
    fn into_u64(self) -> u64;   // always returns a 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 code doesn't work because u64 also happens to be the first type of the list. Use a different first type like i64 or the non-legacy format instead.

Alternative Format

An alternative format is also supported when the in_format feature is enabled:

trait-gen = { version="1.1", features=["in_format"] }

Warning: This feature is temporary, and there is no guarantee that it will be maintained.

Here, in is used instead of an arrow ->, and the argument types must be between square brackets:

use trait_gen::trait_gen;

#[trait_gen(T in [u8, u16, u32, u64, u128])]
impl MyLog for T {
    fn my_log2(self) -> u32 {
        T::BITS - 1 - self.leading_zeros()
    }
}

Using this format issues 'deprecated' warnings that you can turn off by adding the #![allow(deprecated)] directive at the top of the file or by adding #[allow(deprecated)] where the generated code is used.

The square brackets are optional since version 1.1: #[trait_gen(T in u8, u16)] is valid.

IDE Code Awareness

rust-analyzer supports procedural macros for code awareness, so everything should be fine for editors based on this Language Server Protocol implementation.

IDEs like RustRover and IntelliJ/CLion with the Rust plugin all "understand" procedural macros. The expansion is shown with "Show macro recursive expansion", among other actions, errors are correctly shown, all the code-awareness features seem to work as expected, and the code disabled by the conditional attribute appears in grey.

Limitations

  • The procedural macro of the trait_gen attribute can't handle scopes, so it doesn't support any type declaration with the same literal as the generic argument. For instance, this code fails to compile because of the generic function:

    use num::Num;
    use trait_gen::trait_gen;
    
    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 add_mod(self, rhs: Self, modulo: Self) -> Self::Output {
            fn int_mod<T: Num> (a: T, m: T) -> T { // <== ERROR, conflicting 'T'
                a % m
            }
            int_mod(self + rhs, modulo)
        }
    }
  • The generic argument must be a type path; it cannot be a more complex type like a reference or a slice. So you can use gen::T<U> -> ... but not &T -> ....

Compatibility

The trait-gen crate is tested for rustc 1.58.0 and newer on Windows 64-bit and Linux 64/32-bit platforms.

Releases

RELEASES.md keeps a log of all the releases.

License

This code is licensed under either MIT License or Apache License 2.0, at your option.