#dynamic-dispatch #dispatch #object-oriented #multiple #dyn #multimethod

macro double-dyn

Macro for implementing functions with multiple dynamic argument dispatch

2 releases

0.1.1 Mar 11, 2022
0.1.0 Mar 11, 2022

#500 in Procedural macros

MIT/Apache

74KB
1.5K SLoC

Provides a macro for implementing functions with multiple dynamic argument dispatch at runtime.

The [double_dyn!] macro will define the specified trait(s) and emit implementations for all of the provided types, and then emit functions that call the appropriate implementation.

Usage

In your Cargo.toml

[dependencies]
double-dyn = "0.1.1"

Basics

The double_dyn! macro invocation has 3 parts.

  1. Trait names for the A and B traits, along with any subtrait bounds
  2. Function prototypes
  3. Implementations for type pairs, in the form <A, B>

Examples

use double_dyn::double_dyn;

double_dyn!{
    type A: MyTraitA;
    type B: MyTraitB: std::fmt::Display;

    fn multiply(a: &dyn MyTraitA, b: &dyn MyTraitB) -> Box<dyn MyTraitB>;

    impl for <i32, String>
    {
        fn multiply(a: &i32, b: &String) -> Box<dyn MyTraitB> {
            let multiplied_val = *a * b.parse::<i32>().unwrap();
            Box::new(multiplied_val.to_string())
        }
    }

    impl for <[i8, i16, i32, i64, i128], [f32, f64]>
    {
        fn multiply(a: &#A, b: &#B) -> Box<dyn MyTraitB> {
            Box::new((*a as #B) * *b)
        }
    }
}

let val = multiply(&2, &7.5);
assert_eq!(format!("{}", val), "15");

This macro invocation above will define the MyTraitA and MyTraitB traits, and provide implementations for all of the relevant types.

As you can see above, multiple A and/or B types may be specified in using a list in [square brackets].

You may use the concrete types explicitly Within the impl block, or alternatively, #A and #B markers can be used as aliases within the function signature and implementation body, and they will be replaced by the type(s) they represent at compile time.

# use double_dyn::double_dyn;
double_dyn!{
    type A: MyTrait: std::fmt::Display;
    type B: MyTrait;

    fn multiply(a: &dyn MyTrait, b: &dyn MyTrait) -> Box<dyn MyTrait>;

    #[commutative]
    impl for <[i8, i16, i32, i64, i128], [f32, f64]>
    {
        fn multiply(a: &#A, b: &#B) -> Box<dyn MyTrait> {
            Box::new((*a as #B) * *b)
        }
    }
}

let val = multiply(&7.0, &2);
assert_eq!(format!("{}", val), "14");

The same trait may be supplied for both A and B. The A and B arguments may still be of different types within the implementation, however. The macro will attempt to infer which argument is A and which is B from the use of the #A or #B markers but will assume the first &dyn MyTrait argument is A if it is ambiguous.

The #[commutative] attribute will cause an additional implementation to be generated where A is replaced by B and vice-versa.

In the case where the A and B trait is the same, the bounds from the A trait take precedence.

You may declare multiple functions within the same double_dyn macro invocation, and all functions will use the same trait(s). However, every declared function must be implemented in each impl block.

Additional usage examples can be found here in the tests.

Limitations

  • All impls must be in the same double_dyn macro invocation along with the definitions. I'd like to be able to support separating declarations from implementations and allow additional impls to be added as appropriate, but I don't have a robust method to communicate between each macro invocation. This is blocked on this issue.

  • Each double_dyn macro invocation defines a trait or pair of traits. This macro isn't designed to add methods to existing traits. It is possible to use this macro to define a trait, and then make that trait a supertrait of another trait you define, thus allowing double-dyn methods on your trait. But the lack of trait upcasting in the stable compiler is still limiting. Please contact me if you have an idea for how to make things better for adding methods to existing traits.

  • Functions may not have generic arguments. This is a fundamental limitation based on the fact that functions are transformed into trait methods, and the traits need to remain object-safe.

  • impls don't support generic "blanket implementations". A types can never support generic types for the same reason as above; object-safety forbids generics in trait methods. B types could theoretically support blanket implementations but currently the macro doesn't parse where clauses in the impls. Please let me know if this feature is important to you, and I can add it.

  • visibility qualifiers, e.g. pub, must be the same for every function prototype. The visibility will be applied to all generated traits and functions.

  • Passing owned args isn't supported. For example, an arg must be of the form &dyn ATrait, as opposed to Box<dyn ATrait>.

  • Some errors and warnings may be reported multiple times.

Future Vision

I would like to allow the addition of new function implementations via impl blocks that aren't part of the original invocation. In other words, to allow the function signatures to be in part of the code, and allow additional implementations to be added elsewhere. Unfortunately I don't believe this is possible on account of Rust not having an ability to communicate between macro invocations. This is discussed here.

I would also like to include more flexibility for implementing methods on existing traits. See the Limitations section above. I am open to suggestions about what you would find useful.

Acknowledgments

This crate implements a strategy proposed by @h2co3 in this thread.

I learned how to write proc macros by studying the code of @dtolnay, and I borrowed some utility functions from the seq-macro crate.

Dependencies

~105KB