Emulate specialization in stable rust with macro_rules!

Happy Easter everyone,

in my code I have two entities (an electric motor winding and a lamination, to be precise). Both winding and lamination may have multiple variants, which could be represented either by an enum variant or by a "Winding" / "Lamination" trait and corresponding implementations for specific structs. I prefer the latter solution to allow other users to create their own variants.

I now have the following issue: The implementation of a trait function depends on the specific winding / lamination combination. Therefore I created a generic new wrapper struct which holds both winding and lamination:

struct Component<W: Winding, L: Lamination> {
winding: W,
lamination: L,
}

I'd now like to implement the trait function for Component with different implementations depending on the types W and L. As far as I understand, this would be a use case for specialization, however I'd like to have a solution in stable Rust.

I had the idea to use the trait function as a wrapper around other, specific function implementations (with unique names). To achieve this, I created the following MWE:

use paste::paste;

macro_rules! call_specialized_function {
    ($arg1: ty, $arg2: ty) => {
        paste! {
            // Call function
            [<add_ $arg1 _to_ $arg2>]();
        }
    }
}

fn add_i32_to_i32() -> () {
    println!("Added i32 to i32!")
}

fn add_i32_to_f64() -> () {
    println!("Added i32 to f64!")
}

fn main() {
    caller(1i32);
    caller(1.0);
}

fn caller<T,S>(arg1: T, arg2: S) -> () {
    call_specialized_function!(T, S);
}

Playground: Rust Playground

However, this doesn't work because T and S are parsed as literals (i.e. the compiler complains that no function add_T_to_S is defined). If I use call_specialized_function!(i32, f64); instead, the macro works as intended.

As far as I understand, my goal can't be achieved because the macro doesn't know about the types of T and S during expansion. Is there some other way to achieve my goal?

If you don't need a blanket implementation, there's no need for specialization. You can just implement your trait non-generically:

impl MyTrait for Component<WindingA, LaminationA> { ... }
impl MyTrait for Component<WindingA, LaminationB> { ... }
impl MyTrait for Component<WindingB, LaminationA> { ... }
impl MyTrait for Component<WindingB, LaminationB> { ... }
2 Likes

Thanks for your answer! What can I do if I actually need a blanket implementation?

If you can afford enumerating all the specific / concrete impls by hand (and that's what a macro can help with: it can reduce the "copy-pasta"), then go for @2e71828's suggestion, and voilà.

If you'd like for there to be a generic impl (e.g., to cover user-defined types), whilst having special-cased handling for your types, then the situation is a bit more tricky:

  • you can define an extra trait, say DownstreamWinding, that downstream users would have to implement. You'd then have a blanket

    impl<T : ?Sized + DownstreamWinding> Winding for T { … }
    

    blanket impl to write your "default" logic off the user-provided one.

    Your types would not implement it, thence circumventing it, and you'd be able to provide your hard-coded logic instead.

  • You could define a enum-trait hybrid:

    enum Winding {
        // your known types
        Foo(Foo),
        Bar(Bar),
    
        // downstream-provided
        Downstream(Arc<dyn DownstreamWinding + Send + Sync>),
    }
    

    that way, as you handle that enum, you'd be able to intuitively match on the known types, and yet have that fallback branch for an unknown implementor of that DownstreamWinding trait.

    While potentially leading to nicer match / branch-y code, it will result in a runtime-cost overhead.

  • Finally, there is a way to polyfill concrete-type-specialization on stable Rust, provided all the implementors be : 'static / not contain short-lived borrows. In that case, you can use dyn Any downcasting on the generic (albeit concrete, as far as the compiler is concerned) type:

    enum DowncastResult<'r> {
        I32(&'r i32),
        F64(&'r f64),
        Unknown,
    }
    
    impl<'r, T : 'static> From<&'r T> for DowncastResult<'r> {
        fn from (it: &'r T)
          -> DowncastResult<'r>
        {
            use ::core::any::Any;
            if let Some(it) = <dyn Any>::downcast_ref::<i32>(it) {
                return Self::I32(it);
            }
            if let Some(it) = <dyn Any>::downcast_ref::<f64>(it) {
                return Self::F64(it);
            }
            Self::Unknown
        }
    }
    
    fn caller<T : 'static, S : 'static> (arg1: T, arg2: S)
      -> ()
    {
        use DowncastResult::*;
        match ((&arg1).into(), (&arg2).into()) {
            | (I32(_arg1), I32(_arg2)) => println!("Added i32 to i32!"),
            | (I32(_arg1), F64(_arg2)) => println!("Added i32 to f64!"),
            | _ => println!("Something else"),
        }
    }
    

In case it wasn't clear, I think the first of these three options I have listed would be the best one to go for

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.