sigp / superstruct

Rust library for versioned data types
https://sigp.github.io/superstruct/
Apache License 2.0
65 stars 3 forks source link

Generate mapping macros to abstract over variants #19

Closed michaelsproul closed 2 years ago

michaelsproul commented 2 years ago

I've just implemented this pattern by hand, which comes up a lot when using superstruct:

#[macro_export]
macro_rules! map_beacon_block_variants {
    ($block:expr, $e:expr) => {
        match $block {
            BeaconBlock::Base(inner) => {
                let f: fn(BeaconBlockBase<_, _>) -> _ = $e;
                BeaconBlock::Base(f(inner))
            }
            BeaconBlock::Altair(inner) => {
                let f: fn(BeaconBlockAltair<_, _>) -> _ = $e;
                BeaconBlock::Altair(f(inner))
            }
            BeaconBlock::Merge(inner) => {
                let f: fn(BeaconBlockMerge<_, _>) -> _ = $e;
                BeaconBlock::Merge(f(inner))
            }
        }
    };
}

impl<E: EthSpec> From<BeaconBlock<E, FullPayload<E>>> for BeaconBlock<E, BlindedPayload<E>> {
    fn from(block: BeaconBlock<E, FullPayload<E>>) -> Self {
        map_beacon_block_variants!(block, |inner| inner.into())
    }
}

I think we could generate the map_beacon_block_variants macro using superstruct, and maybe some other macros with a similar structure, like mapping into a common type rather than Self.

michaelsproul commented 2 years ago

Having played with this some more I think we can make a single generic macro by passing in the constructor :heart_eyes:

#[macro_export]
macro_rules! map_beacon_block {
    ($block:expr, $e:expr) => {
        match $block {
            BeaconBlock::Base(inner) => {
                let f: fn(
                    BeaconBlockBase<_, _>,
                    fn(BeaconBlockBase<_, _>) -> BeaconBlock<_, _>,
                ) -> _ = $e;
                f(inner, BeaconBlock::Base)
            }
            BeaconBlock::Altair(inner) => {
                let f: fn(
                    BeaconBlockAltair<_, _>,
                    fn(BeaconBlockAltair<_, _>) -> BeaconBlock<_, _>,
                ) -> _ = $e;
                f(inner, BeaconBlock::Altair)
            }
            BeaconBlock::Merge(inner) => {
                let f: fn(
                    BeaconBlockMerge<_, _>,
                    fn(BeaconBlockMerge<_, _>) -> BeaconBlock<_, _>,
                ) -> _ = $e;
                f(inner, BeaconBlock::Merge)
            }
        }
    };
}

Then at the use site the caller can choose to ignore or use the variant constructor as they wish:

fn from(block: BeaconBlock<E, FullPayload<E>>) -> Self {
    map_beacon_block!(block, |inner, cons| {
        let (block, payload) = inner.into();
        (cons(block), payload)
    })
}
realbigsean commented 2 years ago

Love it!