Implementing a struct with a macro_rule

I want to implement a struct with a macro_rule. This is largely because the trait based generics require a lot of boilerplate and trait hunting.

The struct in question has a hash table inside but the key and the value types are to be provided by the user. The code is as follows:

macro_rules! new_ytz {
    ($T: ty) => {
        // define the struct
        pub struct Ytz {
            table: hashbrown::hash_map::HashMap<$T, $T>,
        }
        
        impl Ytz {
            pub fn new() -> Self {
                Ytz {
                    table: hashbrown::hash_map::HashMap::<$T, $T>::new(),
                }
            }
        
            pub fn add(&mut self, item: &$T) {
                if self.table.contains_key(item) {
                    *self.table.get_mut(item).unwrap() += *item;
                } else {
                    self.table.insert(*item, *item);
                }
            }
        
            pub fn largest(&self) -> $T {
                let mut result = 0;
                for v in self.table.values() {
                    if result < *v {
                        result = *v;
                    }
                }
                result
            }
        }
        // construct an instance of the struct and return it
        Ytz::new()
    };
}

// driver
fn main() {
    let mut y = new_ytz!(u64); // should construct the object and return Ytz::new()
    y.add(&71);
    y.add(&25);
    y.add(&25);
    y.add(&25);
    y.add(&34);
    println!("{}", y.largest());
}

Obviously it won't compile since it tries to paste the struct within the main function. How can I work-around it? Meaning how can I paste the struct outside the main function publicly, along with the impl block?

You just need to put another set of curly braces around the result of the macro so that it is a block expression:

macro_rules! new_ytz {
    ($T: ty) => {{
        // define the struct
        pub struct Ytz {
            table: hashbrown::hash_map::HashMap<$T, $T>,
        }
        
        impl Ytz {
            pub fn new() -> Self {
                Ytz {
                    table: hashbrown::hash_map::HashMap::<$T, $T>::new(),
                }
            }
        
            pub fn add(&mut self, item: &$T) {
                if self.table.contains_key(item) {
                    *self.table.get_mut(item).unwrap() += *item;
                } else {
                    self.table.insert(*item, *item);
                }
            }
        
            pub fn largest(&self) -> $T {
                let mut result = 0;
                for v in self.table.values() {
                    if result < *v {
                        result = *v;
                    }
                }
                result
            }
        }
        // construct an instance of the struct and return it
        Ytz::new()
    }};
}

// driver
fn main() {
    let mut y = new_ytz!(u64); // should construct the object and return Ytz::new()
    y.add(&71);
    y.add(&25);
    y.add(&25);
    y.add(&25);
    y.add(&34);
    println!("{}", y.largest());
}

version on the playground with std's HashMap (hashbrown isn't available on the playground)

2 Likes

This works but the Ytz struct is not available publicly. For example if a function uses Ytz and wants to return it, it's not available in the module.

Declarations of types in local blocks like that are only really intended to be used locally, as an implementation detail of generating the value that the block returns. If you want to declare global types, the best solution is to have a declare_ytz macro to declare it and then create instances of the corresponding type as necessary. Here's an updated version of the playground showing this approach. For full generality, you probably want to provide the struct name as a parameter to the macro as well, so you don't end up redeclaring the same struct.

3 Likes

Nice, although I wish the decl macro wouldn't have to be manually called. Something like y = new_ytz!(u64 => u64); and nothing else.

macro_rules! declare_ytz {
    ($struct_name:ident; $T_Key:ty => $T_Value:ty) => {
        // define the struct
        pub(crate) struct $struct_name {
            table: std::collections::HashMap<$T_Key, $T_Value>,
        }
        
        impl $struct_name {
            pub fn new() -> Self {
                Self {
                    table: std::collections::HashMap::<$T_Key, $T_Value>::new(),
                }
            }
        
            pub fn add(&mut self, item: &$T_Key) {
                if self.table.contains_key(item) {
                    *self.table.get_mut(item).unwrap() += *item as $T_Value;
                } else {
                    self.table.insert(*item, *item as $T_Value);
                }
            }
        
            pub fn largest(&self) -> $T_Value {
                let mut result = 0;
                for v in self.table.values() {
                    if result < *v {
                        result = *v;
                    }
                }
                result
            }
        }
        
    };
}

declare_ytz!(YtzU64; u64 => u64);

// driver
fn main() {
    let mut y = YtzU64::new(); // should construct the object and return Ytz::new()
    y.add(&71);
    y.add(&25);
    y.add(&25);
    y.add(&25);
    y.add(&34);
    println!("{}", y.largest());
}

If you want to define types within the body of a function and yet be able to return them, you will need to use at least a trait defined outside the function body and then use impl Trait as the (erased) type in the signature of the function:

pub
trait Ytz<T> {
    fn new () -> Self
    where
        Self : Sized,
    ;
    fn add (self: &'_ mut Self, item: &'_ T)
    ;
    fn largest (self: &'_ Self) -> T
    ;
}

macro_rules! new_ytz {
    ($T: ty) => {{
        // define the struct
        struct Ytz {
            table: std::collections::HashMap<$T, $T>,
        }
        
        impl $crate::Ytz<$T> for Ytz {
            // ...
        }
        // construct an instance of the struct and return it
        Ytz::new()
    }};
}

// driver
fn mk_ytz () -> impl Ytz<u64>
{
    let mut y = new_ytz!(u64); // should construct the object and return Ytz::new()
    y.add(&71);
    y.add(&25);
    y.add(&25);
    y.add(&25);
    y.add(&34);
    y
}

fn main ()
{
    let y = mk_ytz();
    println!("{}", y.largest());
}

But then at that point we start to see a XY problem: why use a macro at all? Your issue can be solved with generics:

use ::hashbrown::hash_map::HashMap as Map;

trait_alias! {
    pub
    trait IsNumber =
        + ::core::ops::AddAssign
        + Copy
        + ::core::fmt::Debug
        + ::core::fmt::Display
        + Eq
        + ::core::hash::Hash
        + Ord
        + ::num_traits::Num
    ;
}

pub
struct Ytz<T>
where
    T : IsNumber,
{
    table: Map<T, T>,
}

impl<T> Ytz<T>
where
    T : IsNumber,
{
    pub
    fn new () -> Self
    {
        Self {
            table: Map::new(),
        }
    }
    
    pub
    fn add (self: &'_ mut Self, item: &'_ T)
    {
        if let Some(slot) = self.table.get_mut(item) {
            *slot += *item;
        } else {
            self.table.insert(*item, *item);
        }
        
    }
    
    pub
    fn largest (self: &'_ Self) -> T
    {
        self.table
            .values()
            .fold(T::zero(), |result, &v| ::core::cmp::max(result, v))
    }
}

// driver
fn mk_ytz () -> Ytz<u64>
{
    let mut y = Ytz::<u64>::new();
    y.add(&71);
    y.add(&25);
    y.add(&25);
    y.add(&25);
    y.add(&34);
    y
}

fn main ()
{
    let y = mk_ytz();
    println!("{}", y.largest());
}
1 Like