Simplify Enum Implementation of Versioned Structures

Hi There! I would like to find a method to simplify the following code, to avoid that match statement which is a bit ugly. Do you have any suggestions for this simplification?

pub enum TransactionEx {
	V1 {
		tx: TransactionV1,
	},
	V2 {
		tx: TransactionV2,
	}
}

impl Committed for TransactionEx {
	fn inputs_committed(&self) -> Vec<Commitment> {
		// >>>> **this part looks a bit ugly, any suggestion to simplify this?** <<<<
		match self {
			TransactionEx::V1{ tx } => tx.inputs_committed(),
			TransactionEx::V2{ tx } => tx.inputs_committed(),
		}
	}
}

impl Committed for TransactionV1 {
	fn inputs_committed(&self) -> Vec<Commitment> {
		// body...
	}
}

impl Committed for TransactionV2 {
	fn inputs_committed(&self) -> Vec<Commitment> {
		// different body...
	}
}

Assuming you just don't like the verbose repetition of the enum name:

impl Committed for TransactionEx {
    fn inputs_committed(&self) {
        use TransactionEx as Tr;
        match self {
            Tr::V1 { tx } => tx.inputs_committed(),
            Tr::V2 { tx } => tx.inputs_committed(),
        }
    }
}

In your case all variants seem to contain a struct, so you could also do:

impl Committed for TransactionEx {
    fn inputs_committed(&self) {
        use TransactionEx::*;
        match self {
            V1 { tx } => tx.inputs_committed(),
            V2 { tx } => tx.inputs_committed(),
        }
    }
}

However do not use the wildcard for enums with variants that don't carry extra data.

To see why, run this. Then comment out line 6 (the use statement) and run it again. You may be surprised by what happens.

Thanks @m51, but actually I like to simplify the 3-lines of match as 1 line, is it possible?

		match self {
			TransactionEx::V1{ tx } => tx.inputs_committed(),
			TransactionEx::V2{ tx } => tx.inputs_committed(),
		}

Long story short: no that isn't possible, because the field needs to be extracted regardless of which enum variant is used. Which in turn means a match or one of its syntactic relatives.

You can write some macros for this use case:

// Apply a match body to all the variants (must all be single value tuples)
macro_rules! match_all {
    ($on:expr, $enum:ident, $with:ident, $body:tt, $($var:ident),*) => {
        match $on {
            $(
                $enum::$var($with) => { $body },
            )*
        }
    }
}

// TransactionEx-specific version, will need updated as variants are added
macro_rules! dispatch_transaction_ex {
    ($on:expr, $with:ident, $body:tt) => {
        match_all!($on, TransactionEx, $with, $body, V1, V2)
    }
}

// The macro in action
impl Committed for TransactionEx {
    fn inputs_committed(&self) -> Vec<Commitment> {
        dispatch_transaction_ex!(self, tx, { tx.inputs_committed() })
    }
}

(May need adjusted depending on what exactly you need.)

2 Likes

Thanks @quinedot ~ This is a solution but I'm not a fans of macros since the macro code is not friendly for reading.

Is it really no other option to simplify this code?

suem,

Enum variants are not types, so any code dependent on selecting the correct one is going to need flow control, i.e. match, if, or if let.

You could replace the enum with separate versioned structs and have them each implement a trait and make functions using them generic.

1 Like

There's a difference between simplicity of the code and you not liking the use of a match. The trait-based alternative is viable, but more verbose and complex. So the code already is as simple as it can ever be.

3 Likes

A middle ground is to provide access through the enum to some dyn Trait object that all of the variants can satisfy:

impl<'a> Borrow<dyn Committed + 'a> for TransactionEx
{
    #[inline]
    fn borrow(&self) -> &(dyn Committed + 'a) {
        match self {
            Self::V1 { tx } => tx,
            Self::V2 { tx } => tx,
        }
    }
}

The inline directive is there to encourage the optimizer to remove the extra vtable indirection.

2 Likes

As others have pointed out, already, your version is as good as it gets. The whole point of writing the match inside the enum trait implementation is, that you don't have to write the match outside of the enum, anymore. The match has to be written somewhere. Rust doesn't have any language-specific features to help you bridge between the low-level implementation details and the high-level usage for traits + enums except macros (I don't recommend dynamic dispatch and relying on the compiler to optimize it away). That's your task as a programmer.

Also, I wouldn't avoid macros for this particular task. You'll have to write the delegating match expression for every trait method and that will become very repetetive, unless you only have this single trait method to implement. I would keep the macro simpler than what was suggested, tho.

/// # Example
///
/// ```no_run
/// fn inputs_commited(&self) -> Vec<Commitment> {
///     apply!({ tx.inputs_committed() })
/// }
/// ```
macro_rules! apply {
    ($body:tt) => {
        match self {
             Self::V1 { tx } => $body,
             Self::V2 { tx } => $body,
        }
    }
}

You might be able to omit the curly brackets in this case, but I'm not sure, as I'm not an expert (I just copied the macro from @quinedot and edited it).

1 Like

There are hygiene restrictions around variable names like self and tx in macros.

(Incidentally, does anyone have a link to a reference on macro hygiene handy? It seems rather under-documented.)

1 Like

With enum_dispatch:

use enum_dispatch::enum_dispatch;

pub struct Commitment; 

#[enum_dispatch]
pub trait Committed {
    fn inputs_committed(&self) -> Vec<Commitment>;
}

#[enum_dispatch(Committed)]
pub enum TransactionEx {
	V1(TransactionV1),
	V2(TransactionV2),
}

pub struct TransactionV1;
pub struct TransactionV2;

impl Committed for TransactionV1 {
	fn inputs_committed(&self) -> Vec<Commitment> {
        // body...
        println!("Hello, world!");
        vec![]
	}
}

impl Committed for TransactionV2 {
	fn inputs_committed(&self) -> Vec<Commitment> {
		// different body...
        vec![]
	}
}

fn main() {
    TransactionEx::V1(TransactionV1).inputs_committed();
    // -> Hello, world!
}
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.