Home > front end >  Generic implementation across the variants of an enum
Generic implementation across the variants of an enum

Time:09-17

I have an enum whose variants have the same structure (these variants could switch roles) and which occurs in a large data structure.

I need to do certain calculations (a fairly long program) by swapping the role of variants. Due to the amount of data, I should avoid doing this permutation directly on the data.

In order to minimize the length of the code and to make future evolutions more reliable, I decided to program things in a generic way.

So far I've tried something with macros. Following code is an example of the generic implementation I have made across variants: (playground)

// enum with same variants shapes
#[derive(Debug, Clone)]
enum Choice{ 
    A{ val: f32, }, 
    B{ val: f32, }, 
}
use Choice::{A,B,};

// code to be implemented
trait MyTrait<const P: usize> { // P is a permutation parameter
    fn eval(&self) -> f32;
    fn zoom(&mut self);
    fn transmute(self) -> Self;
}

// variant permutation macros
macro_rules! choice_a { 
    (0 | val: $a:ident) => (A{val: $a}); 
    (1 | val: $a:ident) => (B{val: $a});
    (0 | val: ref $a:ident) => (A{val: ref $a}); 
    (1 | val: ref $a:ident) => (B{val: ref $a});
    (0 | val: ref mut $a:ident) => (A{val: ref mut $a}); 
    (1 | val: ref mut $a:ident) => (B{val: ref mut $a});
    (0 | val: $a:expr) => (A{val: $a}); 
    (1 | val: $a:expr) => (B{val: $a});
}
macro_rules! choice_b { 
    (0 | val: $a:ident) => (B{ val: $a}); 
    (1 | val: $a:ident) => (A{ val: $a});
    (0 | val: ref $a:ident) => (B{ val: ref $a}); 
    (1 | val: ref $a:ident) => (A{ val: ref $a});
    (0 | val: ref mut $a:ident) => (B{ val: ref mut $a}); 
    (1 | val: ref mut $a:ident) => (A{ val: ref mut $a});
    (0 | val: $a:expr) => (B{ val: $a}); 
    (1 | val: $a:expr) => (A{ val: $a}); 
}

// macro for generic implementation
macro_rules! mytrait {
    ($p:tt) => (
        impl MyTrait<$p> for Choice {
            fn eval(&self) -> f32 {
                match *self {
                    choice_a!{$p | val: ref va} => { va.powi(2) },
                    choice_b!{$p | val: ref va} => { va.sqrt() },
                }
            }
            fn zoom(&mut self) {
                match *self {
                    choice_a!{$p | val: ref mut va} => { *va *= 2.0; },
                    choice_b!{$p | val: ref mut va} => { *va /= 2.0; },
                }
            }
            fn transmute(self) -> Self {
                match self {
                    choice_a!{$p | val: va} => { choice_b!{$p | val: va   1.0} },
                    choice_b!{$p | val: va} => { choice_a!{$p | val: va - 1.0} },
                }
            }
        }
    );
}
mytrait!(0); // no permutation case
mytrait!(1); // permutation case

fn main() {
    
    for x in vec![Choice::A{ val: 16.0}, Choice::B{ val: 16.0}] {
        let mut y = x.clone();
        println!("Variants roles are not permuted:");
        println!("y = {:?}", y);
        println!("eval y = {:?}", MyTrait::<0>::eval(&y));
        MyTrait::<0>::zoom(&mut y);
        println!("zoomed y = {:?}", y);
        let y = MyTrait::<0>::transmute(y);
        println!("transmuted zoomed y = {:?}", y);
        println!("-------------------------");
        println!("Variants roles are permuted:");
        let mut z = x;
        println!("z = {:?}", z);
        println!("eval z = {:?}", MyTrait::<1>::eval(&z));
        MyTrait::<1>::zoom(&mut z);
        println!("zoomed z = {:?}", z);
        let z = MyTrait::<1>::transmute(z);
        println!("transmuted zoomed z = {:?}", z);
        println!("=====================================");
    }
    
}

It results in:

Variants roles are not permuted:
y = A { val: 16.0 }
eval y = 256.0
zoomed y = A { val: 32.0 }
transmuted zoomed y = B { val: 33.0 }
-------------------------
Variants roles are permuted:
z = A { val: 16.0 }
eval z = 4.0
zoomed z = A { val: 8.0 }
transmuted zoomed z = B { val: 7.0 }
=====================================
Variants roles are not permuted:
y = B { val: 16.0 }
eval y = 4.0
zoomed y = B { val: 8.0 }
transmuted zoomed y = A { val: 7.0 }
-------------------------
Variants roles are permuted:
z = B { val: 16.0 }
eval z = 256.0
zoomed z = B { val: 32.0 }
transmuted zoomed z = A { val: 33.0 }
=====================================

My code works but is not completely satisfactory. Indeed, my choice_a! and choice_b! macros are not evolutive, are rigid in their use, and produce either an entire pattern or a complete definition of the enum. It would be better if these macros only produced the name of the enum. For example, it would be better to be able to write choice_a!($p) { val: va - 1.0} rather than choice_a!{$p | val: va - 1.0}, and it would be better to be able to write choice_b!($p){ val: ref mut va} rather than choice_b!{$p | val: ref mut va}.

Is it possible to do this? Are there other methods besides macros to do this kind of generic implementation across variants?

CodePudding user response:

Not sure if I am commenting on the thing that the question is trying to solve (so if I am off beat, please disregard this answer). But wouldn't it be easier (or at least more readable) to avoid macros completely and try something like this instead?


// enum with same variants shapes
#[derive(Debug, Clone)]
enum Choice{
    A{ val: f32, },
    B{ val: f32, },
}
use Choice::{A,B,};

// code to be implemented
trait MyTrait<const P: bool> { // P is a permutation parameter
fn eval(&self) -> f32;
    fn zoom(&mut self);
    fn transmute(self) -> Self;
}

impl Choice {

    fn as_val(&self) -> &f32 {
        match self {
            Choice::A { val } => val,
            Choice::B { val } => val,
        }
    }

    fn as_val_mut(&mut self) -> &mut f32 {
        match self {
            Choice::A { val } => val,
            Choice::B { val } => val,
        }
    }

    /// If P=false, primary choice is A, secondary B. If P=true, the values are switched.
    fn is_primary<const P: bool>(&self) -> bool {
        match self {
            Choice::A { .. } => !P,
            Choice::B { .. } => P,
        }
    }

    fn flip_type(self) -> Choice {
        match self {
            Choice::A { val } => Choice::B { val },
            Choice::B { val } => Choice::A { val }
        }
    }

}

impl <const P: bool> MyTrait<P> for Choice {
    fn eval(&self) -> f32 {
        if self.is_primary::<P>() {
            self.as_val().powi(2)
        } else {
            self.as_val().sqrt()
        }
    }

    fn zoom(&mut self) {
        if self.is_primary::<P>() {
            *self.as_val_mut() *= 2.0;
        } else {
            *self.as_val_mut() /= 2.0;
        }
    }

    fn transmute(self) -> Self {
        let is_primary = self.is_primary::<P>();
        let mut copy = self.flip_type();
        if is_primary {
            *copy.as_val_mut()  = 1.0;
        } else {
            *copy.as_val_mut() -= 1.0;
        };
        copy
    }
}


fn main() {

    for x in vec![Choice::A{ val: 16.0}, Choice::B{ val: 16.0}] {
        let mut y = x.clone();
        println!("Variants roles are not permuted:");
        println!("y = {:?}", y);
        println!("eval y = {:?}", MyTrait::<false>::eval(&y));
        MyTrait::<false>::zoom(&mut y);
        println!("zoomed y = {:?}", y);
        let y = MyTrait::<false>::transmute(y);
        println!("transmuted zoomed y = {:?}", y);
        println!("-------------------------");
        println!("Variants roles are permuted:");
        let mut z = x;
        println!("z = {:?}", z);
        println!("eval z = {:?}", MyTrait::<true>::eval(&z));
        MyTrait::<true>::zoom(&mut z);
        println!("zoomed z = {:?}", z);
        let z = MyTrait::<true>::transmute(z);
        println!("transmuted zoomed z = {:?}", z);
        println!("=====================================");
    }

}

This code gives you the same results. The is_primary check should compile to basically the same thing as your original match macros, so performance should be similar (but I can check if you want to be sure). It is slightly longer, but overall I find it much more readable, and the new methods can be also useful outside of implementing MyTrait.

CodePudding user response:

Answer with a simple macro improvement:(playground)

// enum with same variants shapes
#[derive(Debug, Clone)]
enum Choice{ 
    A{ val: f32, }, 
    B{ val: f32, }, 
}
use Choice::{A,B,};

// variant permutation macros; space before * is required!
macro_rules! a { 
    (0 | $($t:tt) *) => (A{$($t) *}); 
    (1 | $($t:tt) *) => (B{$($t) *});
}
macro_rules! b { 
    (0 | $($t:tt) *) => (B{$($t) *}); 
    (1 | $($t:tt) *) => (A{$($t) *});
}

// code to be implemented
trait MyTrait<const P: usize> { // P is a permutation parameter
    fn eval(&self) -> f32;
    fn zoom(&mut self);
    fn transmute(self) -> Self;
}

// macro for generic implementation
macro_rules! mytrait {
    ($p:tt) => (
        impl MyTrait<$p> for Choice {
            fn eval(&self) -> f32 {
                match *self {
                    a!{$p | val: ref va} => { va.powi(2) },
                    b!{$p | val: ref va} => { va.sqrt() },
                }
            }
            fn zoom(&mut self) {
                match *self {
                    a!{$p | val: ref mut va} => { *va *= 2.0; },
                    b!{$p | val: ref mut va} => { *va /= 2.0; },
                }
            }
            fn transmute(self) -> Self {
                match self {
                    a!{$p | val: va} => { b!{$p | val: va   1.0} },
                    b!{$p | val: va} => { a!{$p | val: va - 1.0} },
                }
            }
        }
    );
}
mytrait!(0); // no permutation case
mytrait!(1); // permutation case

Previous answer:

Replace enum Choice{ A{ val: f32, }, B{ val: f32, }, } by type Choice = (Variant,Data) with enum Variant{ A, B, } and struct Data { val: f32 }: (playground)

// variant label
#[derive(Debug, Clone, Copy,)]
enum Variant{ A, B, }
use Variant::{A,B,};
// data fields
#[derive(Debug, Clone,)]
struct Data { val: f32 }
// enum like type
type Choice = (Variant,Data);


macro_rules! a { (0) => (A); (1) => (B); }
macro_rules! b { (0) => (B); (1) => (A); }

// code to be implemented
trait MyTrait<const P: usize> { // P is a permutation parameter
    fn eval(&self) -> f32;
    fn zoom(&mut self);
    fn transmute(self) -> Self;
}

// macro for generic implementation
macro_rules! mytrait {
    ($p:tt) => (
        impl MyTrait<$p> for Choice {
            fn eval(&self) -> f32 {
                match *self {
                    (a!($p), Data{val: ref v}) => { v.powi(2) },
                    (b!($p), Data{val: ref v}) => { v.sqrt() },
                }
            }
            fn zoom(&mut self) {
                match *self {
                    (a!($p), Data{val: ref mut v}) => { *v *= 2.0; },
                    (b!($p), Data{val: ref mut v}) => { *v /= 2.0; },
                }
            }
            fn transmute(self) -> Self {
                match self {
                    (a!($p), Data{val: v}) => (b![$p], Data{val: v   1.0}),
                    (b!($p), Data{val: v}) => (a![$p], Data{val: v - 1.0}),
                }
            }
        }
    );
}
mytrait!(0); // no permutation case
mytrait!(1); // permutation case
  • Related