Crate duplicate

source ·
Expand description

This crate provides macros for easy code duplication with substitution:

The only major difference between the two is where you can use them. Therefore, the following section presents how to use duplicate_item only. Refer to duplicate’s documentation for how it defers from what is specified below.

Usage

Say you have a trait with a method is_max that should return true if the value of the object is the maximum allowed and false otherwise:

trait IsMax {
  fn is_max(&self) -> bool;
}

You would like to implement this trait for the three integer types u8, u16, and u32:

impl IsMax for u8 {
  fn is_max(&self) -> bool {
    *self == 255
  }
}
impl IsMax for u16 {
  fn is_max(&self) -> bool {
    *self == 65_535
  }
}
impl IsMax for u32 {
  fn is_max(&self) -> bool {
    *self == 4_294_967_295
  }
}

This is a lot of repetition. Only the type and the maximum value are actually different between the three implementations. This might not be much in our case, but imagine doing this for all the integer types (10, as of the last count.) We can use the duplicate_item attribute to avoid repeating ourselves:

use duplicate::duplicate_item;
#[duplicate_item(
  int_type  max_value;
  [ u8 ]    [ 255 ];
  [ u16 ]   [ 65_535 ];
  [ u32 ]   [ 4_294_967_295 ];
)]
impl IsMax for int_type {
  fn is_max(&self) -> bool {
    *self == max_value
  }
}

assert!(!42u8.is_max());
assert!(!42u16.is_max());
assert!(!42u32.is_max());

The above code will expand to the three implementations before it. The attribute invocation specifies that the following item should be substituted by three duplicates of itself. Additionally, each occurrence of the identifier int_type in the first duplicate should be replaced by u8, in the second duplicate by u16, and in the last by u32. Likewise, each occurrence of max_value should be replaced by 255, 65_535, and 4_294_967_295 in the first, second, and third duplicates respectively.

int_type and max_value are called substitution identifiers, while [ u8 ], [ u16 ], and [ u32 ] are each substitutions for int_type and [255], [65_535], and [4_294_967_295] are substitutions for max_value. Each pair of substitutions for the identifiers is called a substitution group. Substitution groups must be seperated by ; and the number of duplicates made is equal to the number of subsitution groups.

Substitution identifiers must be valid Rust identifiers. The code inside substitutions can be arbitrary, as long as the expanded code is valid.

Parameterized Substitution

Say we have a struct that wraps a vector and we want to give access to the vector’s get and get_mut methods directly:

struct VecWrap<T>(Vec<T>);

impl<T> VecWrap<T> {
  pub fn get(&self, idx: usize) -> Option<&T> {
    self.0.get(idx)
  }
  pub fn get_mut(&mut self, idx: usize) -> Option<&mut T> {
    self.0.get_mut(idx)
  }
}

let mut vec = VecWrap(vec![1,2,3]);
assert_eq!(*vec.get(0).unwrap(), 1);
*vec.get_mut(1).unwrap() = 5;
assert_eq!(*vec.get(1).unwrap(), 5);

Even though the implementations of the two versions of get are almost identical, we will always need to duplicate the code, because Rust cannot be generic over mutability. Parameterized substitution allows us to pass code snippets to substitution identifiers to customize the substitution for that specific use of the identifier. We can use it to help with the implementation of constant and mutable versions of methods and functions. The following impl is identical to the above code:

impl<T> VecWrap<T> {
  #[duplicate_item(
    method     reference(type);
    [get]      [& type];
    [get_mut]  [&mut type];
  )]
  pub fn method(self: reference([Self]), idx: usize) -> Option<reference([T])> {
    self.0.method(idx)
  }
}

In a duplicate_item invocation, if a substitution identifier is followed by parenthises containing a list of parameters, they can be used in the substitution. In this example, the reference identifier takes 1 parameter named type, which is used in the substitutions to create either a shared reference to the type or a mutable one. When using the reference in the method declaration, we give it different types as arguments to construct either shared or mutable references. E.g. reference([Self]) becomes &Self in the first duplicate and &mut Self in the second. An argument can be any code snippet inside [].

A substitution identifier can take any number of parameters. We can use this if we need to also provide the references with a lifetime:

impl<T> VecWrap<T> {
  #[duplicate_item(
    method     reference(lifetime, type);
    [get]      [& 'lifetime type];
    [get_mut]  [& 'lifetime mut type];
  )]
  pub fn method<'a>(self: reference([a],[Self]),idx: usize) -> Option<reference([a],[T])> {
    self.0.method(idx)
  }
}

Here we pass the lifetime 'a to the substitution as the first argument, and the type as the second. Notice how the arguments are separated by a comma. This results in the following code:

impl<T> VecWrap<T> {
  pub fn get<'a>(self: &'a Self, idx: usize) -> Option<&'a T> {
    self.0.get(idx)
  }
  pub fn get_mut<'a>(self: &'a mut Self, idx: usize) -> Option<&'a mut T> {
    self.0.get_mut(idx)
  }
}

Notice also the way we pass lifetimes to identifiers: reference([a], [Self]). The lifetime is passed without the ' prefix, which is instead present in the substitution before the lifetime: [& 'lifetime type]. This is because the rust syntax disallows lifetimes in brackets on their own. Our solution is therefore a hacking of the system and not a property of duplicate_item itself.

Nested Invocation

Imagine we have the following trait with the method is_negative that should return true if the value of the object is negative and false otherwise:

trait IsNegative {
  fn is_negative(&self) -> bool;
}

We want to implement this for the six integer types u8, u16, u32, i8, i16, and i32. For the first three types, which are all unsigned, the implementation of this trait should trivially return false as they can’t be negative. However, for the remaining, signed types their implementations is identical (checking whether they are less than 0), but, of course, different from the first three:

impl IsNegative for u8 {
  fn is_negative(&self) -> bool {
    false
  }
}
impl IsNegative for u16 {
  fn is_negative(&self) -> bool {
    false
  }
}
impl IsNegative for u32 {
  fn is_negative(&self) -> bool {
    false
  }
}
impl IsNegative for i8 {
  fn is_negative(&self) -> bool {
    *self < 0
  }
}
impl IsNegative for i16 {
  fn is_negative(&self) -> bool {
    *self < 0
  }
}
impl IsNegative for i32 {
  fn is_negative(&self) -> bool {
    *self <  0
  }
}

Notice how the code repetition is split over 2 axes: 1) They all implement the same trait 2) the method implementations of the first 3 are identical to each other but different to the next 3, which are also mutually identical. To implement this using only the syntax we have already seen, we could do something like this:

#[duplicate_item(
  int_type implementation;
  [u8]     [false];
  [u16]    [false];
  [u32]    [false];
  [i8]     [*self < 0];
  [i16]    [*self < 0];
  [i32]    [*self < 0]
)]
impl IsNegative for int_type {
  fn is_negative(&self) -> bool {
    implementation
  }
}

assert!(!42u8.is_negative());
assert!(!42u16.is_negative());
assert!(!42u32.is_negative());
assert!(!42i8.is_negative());
assert!(!42i16.is_negative());
assert!(!42i32.is_negative());

However, ironically, we here had to repeat ourselves in the macro invocation instead of the code: we needed to repeat the implementations [ false ] and [ *self < 0 ] three times each. We can utilize nested invocation to remove the last bit of repetition:

#[duplicate_item(
  int_type implementation;
  duplicate!{
    [
      int_type_nested; [u8]; [u16]; [u32]
    ]
    [ int_type_nested ] [ false ];
  }
  duplicate!{
    [
      int_type_nested; [i8]; [i16]; [i32]
    ]
    [ int_type_nested ] [ *self < 0 ];
  }
)]
impl IsNegative for int_type {
  fn is_negative(&self) -> bool {
    implementation
  }
}

assert!(!42u8.is_negative());
assert!(!42u16.is_negative());
assert!(!42u32.is_negative());
assert!(!42i8.is_negative());
assert!(!42i16.is_negative());
assert!(!42i32.is_negative());

We use duplicate!{..} to invoke the macro inside itself. In our example, we have 2 invocations that each produce 3 substitution groups, inserting the correct implementation for their signed or unsigned types. The above nested invocation is equivalent to the previous, non-nested invocation, and actually expands to it as an intermediate step before expanding the outer-most invocation.

Deeper levels of nested invocation are possible and work as expected. There is no limit on the depth of nesting, however, as might be clear from our example, it can get complicated to read.

Lastly, we should note that we can have nested invocations interleaved with normal substitution groups. For example, say we want to implement IsNegative for i8, but don’t want the same for i16 and i32. We could do the following:

#[duplicate_item(
  int_type implementation;
  duplicate!{
    [                                     // -+
      int_type_nested; [u8]; [u16]; [u32] //  | Nested invocation producing 3
    ]                                     //  | substitution groups
    [int_type_nested ] [ false ];         //  |
  }                                       // -+
  [ i8 ] [ *self < 0 ]                    // -- Substitution group 4
)]
impl IsNegative for int_type {
  fn is_negative(&self) -> bool {
    implementation
  }
}

In general, nested invocations can be used anywhere. However, note that nested invocations are only recognized by the identifier duplicate, followed by !, followed by a delimiter within which the nested invocation is. Therefore, care must be taken to ensure the surrounding code is correct after the expansion. E.g. maybe ; is needed after the invocation, or commas must be produced by the nested invocation itself as part of a list.

Verbose Syntax

The syntax used in the previous examples is the short syntax. duplicate_item also accepts a verbose syntax that is less concise, but more readable in some circumstances. Using the verbose syntax, the very first example above looks like this:

use duplicate::duplicate_item;
#[duplicate_item(
  [
    int_type  [ u8 ]
    max_value [ 255 ]
  ]
  [
    int_type  [ u16 ]
    max_value [ 65_535 ]
  ]
  [
    int_type  [ u32 ]
    max_value [ 4_294_967_295 ]
  ]
)]
impl IsMax for int_type {
  fn is_max(&self) -> bool {
    *self == max_value
  }
}

In the verbose syntax, a substitution group is put inside ‘[]’ and includes a list of substitution identifiers followed by their substitutions. No ;s are needed. Here is an annotated version of the same code:

#[duplicate_item(
  [                               //-+
    int_type  [ u8 ]              // | Substitution group 1
    max_value [ 255 ]             // |
//  ^^^^^^^^^ ^^^^^^^ substitution   |
//  |                                |
//  substitution identifier          |
  ]                               //-+
  [                               //-+
    int_type  [ u16 ]             // | Substitution group 2
    max_value [ 65_535 ]          // |
  ]                               //-+
  [                               //-+
    max_value [ 4_294_967_295 ]   // | Substitution group 3
    int_type  [ u32 ]             // |
  ]                               //-+
)]

Note that in each substitution group every identifier must have exactly one substitution. All the groups must have the exact same identifiers, though the order in which they arrive in each group is not important. For example, in the annotated example, the third group has the max_value identifier before int_type without having any effect on the expanded code.

The verbose syntax is not very concise but it has some advantages over the short syntax in regards to readability. Using many identifiers and long substitutions can quickly become unwieldy in the short syntax. The verbose syntax deals better with both cases as it will grow horizontally instead of vertically.

The verbose syntax also offers nested invocation. The syntax is exactly the same, but since there is no initial substitution identifier list, nested calls can be used anywhere (though still not inside substitution groups.) The previous IsNegative nested invocation example can be written as follows:

#[duplicate_item(
  duplicate!{
    [ int_type_nested; [u8]; [u16]; [u32] ]
    [
      int_type [ int_type_nested ]
      implementation [ false ]
    ]
  }
  duplicate!{
    [ int_type_nested; [i8]; [i16]; [i32] ]
    [
      int_type [ int_type_nested ]
      implementation [ *self < 0 ]
    ]
  }
)]
impl IsNegative for int_type {
  fn is_negative(&self) -> bool {
    implementation
  }
}

assert!(!42u8.is_negative());
assert!(!42u16.is_negative());
assert!(!42u32.is_negative());
assert!(!42i8.is_negative());
assert!(!42i16.is_negative());
assert!(!42i32.is_negative());

It’s important to notice that the nested invocation doesn’t know it isn’t the outer-most invocation and therefore doesn’t discriminate between identifiers. We had to use a different identifier in the nested invocations (int_type_nested) than in the code (int_type), because otherwise the nested invocation would substitute the substitution identifier too, instead of only substituting in the nested invocation’s substitute.

Nested invocations must produce the syntax of their parent invocation. However, each invocation’s private syntax is free to use any syntax type. Notice in our above example, the nested invocations use short syntax but produce verbose syntax for the outer-most invocation.

Global Substitutions

Say we have a function that takes two types as inputs and returns the same types as output:

fn some_func(
  arg1: Some<Complex<()>, Type<WeDont<Want, To, Repeat>>>,
  arg2: Some<Other, Complex<Type<(To, Repeat)>>>)
  -> (
    Some<Complex<()>, Type<WeDont<Want, To, Repeat>>>,
    Some<Other, Complex<Type<(To, Repeat)>>>
  )
{
  ...
}

Using global substitution, we can avoid repeating the types:

#[duplicate_item(
  typ1 [Some<Complex<()>, Type<WeDont<Want, To, Repeat>>>];
  typ2 [Some<Other, Complex<Type<(To, Repeat)>>>];
)]
fn some_func(arg1: typ1, arg2: typ2) -> (typ1, typ2){
  ...
}

Here we have defined the two global substitution variables typ1 and typ2, and used them in the function definition. Global substitutions have the same syntax as verbose syntax substitution (identifier, optionally followed by parameters, followed by a substitution.) In our example, no short or verbose syntax substitution groups are given. While this is not usually allowed, since we have given at least one global substitution, the item will simply be kept as is, except with the global substitutions.

We can follow global substitutions by substitution groups to achieve duplication too:

#[duplicate_item(
  typ1 [Some<Complex<()>, Type<WeDont<Want, To, Repeat>>>];
  typ2 [Some<Other, Complex<Type<(To, Repeat)>>>];
  method     reference(type);
  [get]      [& type];
  [get_mut]  [&mut type];
)]
fn method(
  arg0: reference([Type<()>]),
  arg1: typ1,
  arg2: typ2)
  -> (reference([typ1]), reference([typ2]))
{
  ...
}

Here we duplicate the function to use either shared or mutable reference, while reusing typ1 and typ2 in both duplicates.

The following additional rules apply when using global substitutions:

  • All global substitutions must come before any short or verbose syntax substitution groups.
  • Global substitution variable are not substituted inside the bodies of following substitutions. If that is needed, multiple invocations can be used.
  • All global substitutions must be separated by ;, also when followed by substitution groups.

Crate Features

module_disambiguation

Implicit Module Name Disambiguation (Enabled by default)

It is sometime beneficial to apply duplicate_item to a module, such that all its contents are duplicated at once. However, this will always need the resulting modules to have unique names to avoid the compiler issueing an error. Without module_disambiguation, module names must be substituted manually. With module_disambiguation, the following will compile successfully:

#[duplicate_item(
  int_type  max_value;
  [ u8 ]    [ 255 ];
  [ u16 ]   [ 65_535 ];
  [ u32 ]   [ 4_294_967_295 ];
)]
mod module {
  impl IsMax for int_type {
    fn is_max(&self) -> bool {
      *self == max_value
    }
  }
  impl IsNegative for int_type {
    fn is_negative(&self) -> bool {
      false
    }
  }
}

assert!(!42u8.is_max());
assert!(!42u16.is_max());
assert!(!42u32.is_max());
assert!(!42u8.is_negative());
assert!(!42u16.is_negative());
assert!(!42u32.is_negative());

This works because the three duplicate modules get assigned unique names: module_u8, module_u16, and module_u32. However, this only works if a substitution identifier can be found, where all its substitutions only produce a single identifier and nothing else. Those identifiers are then converted to snake case, and postfixed to the original module’s name, e.g., module + u8 = module_u8. The first suitable substitution identifier is chosen.

Notes:

  • The exact way unique names are generated is not part of any stability guarantee and should not be depended upon. It may change in the future without bumping the major version.
  • Only the name of the module is substituted with the disambiguated name. Any matching identifier in the body of the module is ignored.

pretty_errors

More Detailed Error Messages (Enabled by default)

Enabling this feature will make error messages indicate exactly where the offending code is. Without this feature, error messages will not provide detailed location indicators for errors.

This feature is has no effect on expansion. Therefore, libraries are advised to keep this feature off (note that it’s enabled by default) to avoid forcing it on users.

Disclaimer

This crate does not try to justify or condone the usage of code duplication instead of proper abstractions. This crate should only be used where it is not possible to reduce code duplication through other means, or where it simply is not worth it.

As an example, libraries that have two or more structs/traits with similar APIs might use this macro to test them without having to copy-paste test cases and manually make the needed edits.

Macros

  • Duplicates the given code and substitutes specific identifiers for different code snippets in each duplicate.

Attribute Macros

  • Duplicates the item and substitutes specific identifiers for different code snippets in each duplicate.