Calculate number of fields that are of type "None" in a struct Rust

Viewed 65

I am working on a macro that computes the number of fields on a struct which are of type None at runtime. I have an implementation that computes the number of fields for a struct. The source code is given below.

use std::rc::Rc;
use std::cell::Cell;

macro_rules! generate {
    ($name:ident {$($field:ident : $t:ty),+}) => {
        struct $name { $($field: $t),+ }
        impl $name {
            fn field_count(&self) -> usize {
                generate!(@count $($field),+)
            }
        }
    };
    (@count $t1:tt, $($t:tt),+) => { 1 + generate!(@count $($t),+) };
    (@count $t:tt) => { 1 };
}

generate! { test1 { num: i32, s: Option<String> }}
generate! { test2 { num: i32, s: String }}

fn main() {
    let s = test1 { num: 0, s: None };
    println!("{}", s.field_count());
    let s = test2 { num: 0, s: String::new() };
    println!("{}", s.field_count());
}

The problem with this implementation is this returns the total number of fields in the struct at compile time so for test1 and test1 the answer is always 2. I am looking for a way to modify the macro to compute fields that are of type "None" in the struct.

1 Answers

To distinguish Nones, we first need to distinguish Options. And this is little hard.

There are two ways to do that: syntactically or via the type system. Syntactically means that the macro will identify fields that have the type Option<T> (i.e. Option < type >). However, this requires a TT munching, will force you to parse the ty fragement type yourself (you cannot rely on it because you'll not be able to inspect into its composed tokens later), and as a bonus won't work if users will type-alias Option.

The type system approach is more robust. The idea is to create some trait, implemented for any type, with a method that for Option<T> will return 1 iff it is Some and for other types will always return 1. This sounds like a perfect use case for specialization (playground):

#![feature(min_specialization)]

pub trait OneIfSome {
    fn one_if_some(&self) -> usize;
}

impl<T> OneIfSome for T {
    default fn one_if_some(&self) -> usize { 1 }
}

impl<T> OneIfSome for Option<T> {
    fn one_if_some(&self) -> usize { self.is_some() as usize }
}

macro_rules! generate {
    ($name:ident {$($field:ident : $t:ty),+}) => {
        struct $name { $($field: $t),+ }
        
        impl $name {
            fn field_count(&self) -> usize {
                generate!(@count self $($field,)*)
            }
        }
    };
    (@count $self:ident $first_field:ident, $($rest:ident,)*) => {
        $crate::OneIfSome::one_if_some(&$self.$first_field)
            + generate!(@count $self $($rest,)*)
    };
    (@count $self:ident) => { 0 };
}

Unfortunately, specialization is unstable, and doesn't even have a clear path towards stabilization. Can we replace it with something stable?

And it turns out, yes! @dtolnay invented a technique called "autoref specialization" that is intended exactly for cases like that (specialization inside a macro). I won't explain how it works here (the linked article is a great read), but here is how you can apply it to your case (playground):

pub trait OneIfSomeGeneral {
    fn one_if_some(&self) -> usize;
}
impl<T> OneIfSomeGeneral for &'_ T {
    fn one_if_some(&self) -> usize { 1 }
}

pub trait OneIfSomeOption {
    fn one_if_some(&self) -> usize;
}
impl<T> OneIfSomeOption for Option<T> {
    fn one_if_some(&self) -> usize { self.is_some() as usize }
}

macro_rules! generate {
    ($name:ident {$($field:ident : $t:ty),+}) => {
        struct $name { $($field: $t),+ }
        
        impl $name {
            fn field_count(&self) -> usize {
                #[allow(unused_imports)]
                use $crate::{OneIfSomeGeneral, OneIfSomeOption};
                
                generate!(@count self $($field,)*)
            }
        }
    };
    (@count $self:ident $first_field:ident, $($rest:ident,)*) => {
        (&$self.$first_field).one_if_some()
            + generate!(@count $self $($rest,)*)
    };
    (@count $self:ident) => { 0 };
}

(Note: There is a problem with the above code: if the field type has a method named one_if_some(), it will take priority. So choose an unambiguous name (__my_generate_macro__one_if_some(), perhaps?)).

Related