How do I get rid of this pattern of extracting a value from an enum

I have a large enum called SupportedMessage that can hold all kinds of messages sent back and forth between a client and server.

pub enum SupportedMessage {
  Invalid(ObjectId),
  DoNothing,
  //... lots of messages such as 
  PublishRequest(PublishRequest),
  PublishResponse(PublishResponse),
  //... and more like it
}

Most of the stack doesn't care what a message is so SupportedMessage is a convenience that allows me to throw them around easily.

But often it'll land in a handler, a bit of code where the type is known and I need to extract it. I often find myself a pattern like this:

if let SupportedMessage::PublishResponse(ref mut response) = publish_response.response {
  //...WORK
} else {
  panic!("Expecting publish response");
}

Is there any way to get rid of the if/else? I want to force the value out and let the system panic for me.

I was thinking of using a macro to contain the logic, but I want to encapsculate the "ref mut" as an arg because sometimes I only need a "ref", or even move the value itself out. I'm not sure if I can do this

e.g.

let response =  supported_message_as!(publish_response.response, PublishResponse, ref mut);
//...WORK

But it gets more complex because ref mut might pose lifetime issues (the ref mut can't live outside the lifetime of the enum) so I might to pass the work into the macro as a closure:

supported_message_as!(publish_response.response, PublishResponse, ref mut, |response| {
  //... WORK
});

At this point the facade to the macro is getting so complex I'm not really saving much. I just don't like that if/else!
Ideas?

1 Like

You write an unwrap like method for each type, some like:

fn unwrap_invalid(self) -> ObjectId {
    use self::SupportedMessage::*;
    match self {
        Invalid(obj) => obj,
        _ => panic!("expected invalid message"),
    }
}
1 Like

Yeah, that's the pattern I've seen. Except you probably don't want to consume self, so take &self or &mut self and return &ObjectId or &mut ObjectId, respectively.

I could add unwraps() but it wouldn't necessarily work for the ref mut case. Also I have 20+ values on the enum so that's a lot of unwraps :slight_smile:

Yeah, you'd need separate methods for immutable and mutable refs. Are you applying this pattern to all 20+ variants though? :slight_smile:

So, you could have 3*N methods:

fn into_publish_request(self) -> PublishRequest;
fn as_publish_request(&self) -> &PublishRequest;
fn as_mut_publish_request(&mut self) -> &mut PublishRequest;

fn into_publish_response(self) -> PublishResponse;
fn as_publish_response(&self) -> &PublishResponse;
fn as_mut_publish_response(&mut self) -> &mut PublishResponse;

...

Macros can help generate the implementations, although you will still need to write out the name of every method at least once due to macro hygiene.

...but, my preferred strategy is to try to keep it down to 3 + N methods (give or take a few!).

The goal is to make it possible to write something like the following:

// (note: these are not to be read as sequential lines of code,
//  but rather, distinct examples)
let x = msg.invalid().expect("expected invalid!");  // <-- by value
let x = msg.as_ref().publish_response().unwrap();   // <-- by ref
let x = msg.as_mut().publish_request().unwrap();    // <-- by mut ref

Here's a rough draft of what I have in mind. Beware; it introduces a generic type which will be unpleasant to have appear in your public API, so I hope SupportedMessage is internal!

Playground link

First, I make a type for every variant. This isn't strictly necessary, but it helps make things more uniform, which could help if you were to try to generate some of the implementations that follow by wrapping your struct definition in a macro.

#[derive(Clone)] struct A(Vec<i32>);
#[derive(Clone)] struct B;
#[derive(Clone)] struct C(String, String);

Next, we define three types for by-value, borrowed, and mutably borrowed variants of our type. All three can be defined in terms of a single generic type:

#[derive(Copy,Clone)]
enum Thing_<A_,B_,C_> {
    A(A_),
    B(B_),
    C(C_),
}

#[derive(Clone)]
struct Thing(Thing_<A,B,C>);

// (not actually sure if it's better for these lifetimes to be the same
//  or different; should hardly matter since it's an enum)
#[derive(Copy,Clone)]
struct ThingRef<'a,'b,'c>(Thing_<&'a A, &'b B, &'c C>);
struct ThingMut<'a,'b,'c>(Thing_<&'a mut A, &'b mut B, &'c mut C>);

Conversions are added from the by-value type to the borrowed ones:

impl Thing {
    fn as_ref<'a>(&'a self) -> ThingRef<'a, 'a, 'a> {
        ThingRef(match self.0 {
            Thing_::A(ref a) => Thing_::A(a),
            Thing_::B(ref b) => Thing_::B(b),
            Thing_::C(ref c) => Thing_::C(c),
        })
    }
    
    fn as_mut<'a>(&'a mut self) -> ThingMut<'a, 'a, 'a> {
        ThingMut(match self.0 {
            Thing_::A(ref mut a) => Thing_::A(a),
            Thing_::B(ref mut b) => Thing_::B(b),
            Thing_::C(ref mut c) => Thing_::C(c),
        })
    }
}

And then we can implement a matching function for each enum variant, similar to Result::{ok,err}. You don't have to make these return Option but I do because it's more versatile than panicking inside (e.g. you can do .unwrap_or_else(|| panic!()) at the callsite for a more useful line number).

This just begs for macro codegen:

impl<A_,B_,C_> Thing_<A_,B_,C_> {
    fn a(self) -> Option<A_> {
        match self {
            Thing_::A(a) => Some(a),
            _ => None,
        }
    }
    
    fn b(self) -> Option<B_> {
        match self {
            Thing_::B(b) => Some(b),
            _ => None,
        }
    }
    
    fn c(self) -> Option<C_> {
        match self {
            Thing_::C(c) => Some(c),
            _ => None,
        }
    }
}

With that, you can use it like this:

fn main() {
    let mut x = Thing(Thing_::A(A(vec![])));
    
    x.as_mut().0.a().unwrap().0.push(0);
    
    assert_eq!(0i32, x.as_ref().0.a().unwrap().0[0]);
}

To improve ergonomics, you could also impl Deref{,mut} for Thing{,Ref,Mut}, which would let you replace e.g. x.0.a() with just x.a(). I didn't bother with this.


Of course, this strategy works much, much better for types that are already meant to be generic, so that you don't have to introduce some terrible hack like Thing_. For instance, this is an entirely reasonable and not-at-all-hacky way for working with a type such as These:

enum These<A,B> {
    JustThis(A),
    JustThat(B),
    ThemBoth(A, B),
}

impl<A,B> These<A,B> {
    // borrowing functions
    fn as_ref<'a>(&'a self) -> These<&'a A, &'a B>;
    fn as_mut<'a>(&'a mut self) -> These<&'a mut A, &'a mut B>;

    // single-variant matchers
    fn just_this(self) -> Option<A>;
    fn just_that(self) -> Option<B>;
    fn them_both(self) -> Option<(A,B)>;

    // ...
}
6 Likes

you could do

impl<'a> From<&'a mut SupportedMessage> for Option<&'a mut PublishResponse> {
    fn from(msg: &'a mut SupportedMessage) -> Self {
        match *msg {
            SupportedMessage::PublishResponse(ref mut resp) => Some(resp),
            _ => None
    }
}

and use it later like this

let response: &mut PublishResponse = Option::from(&mut test_resp).expect("Expecting publish response");
//...WORK

you still would need to write all the impl's for Option<...>, Option<& ...> and Option<&mut ...>`.
maybe try yo write a macro for that?

1 Like

You could also create a macro to create an into, as and as_mut method for each enum variant, seems like a perfect job for macros.

Wow, these are some awesome and detailed replies! I'll see what fits best with my code and many thanks for all the answers.

Also - this would be a lot easier if the 'enum variants as types' RFC ever lands. Then you could pass SupportedMessage::PublishResponse as a function argument directly, and this entire problem disappears.

1 Like