Rust enum match: capture entire arm as a single varaible

pub enum Foo {
  A(u32, f32),
  B{ x: u32, y: f32 }
}

let f: Foo = ... ;
match f {
  A(a, b) => { ... }
  B{x, y} => { ... }
}

Now, in the context of writing procedural macros, it would be nicer if we could capture both arms as 'obj', and index it differently, i.e.

match f {
  A ??? => obj.0, obj.1;
  B ??? => obj.x, obj.y;
}

Is there a way to capture an arm of a enum, then, for unnamed index it with .0, .1, .2, ... and for named, index it with names ?

do you mean like

match f {
    obj @ A => { obj.0; obj.1; },
    obj @ B => { obj.x; obj.y; },
}
1 Like

I don't think there's a way without nesting an actual type in there. And that may hurt size optimization.

pub struct FooB { x: u32, y: f32, }
pub enum Foo {
    A((u32, f32)),
    B(FooB),
}

pub fn f(foo: Foo) {
    match foo {
        Foo::A(a) => println!("{} {}", a.0, a.1),
        Foo::B(b) => println!("{} {}", b.x, b.y),
    }
}

Inside the arm, a and b need to have a type. So Rust would need "variants are types" (not holding my breath), or

  • Automatic conversion between tuple variants and tuples
    • So their layouts must also match
  • Ad-hoc structs (with named fields)
  • Automatic conversion between struct variants and ad-hoc structs
    • So their layouts must also match

Or perhaps some sort of view struct like helper that applies when you know the variant.

The layout thing is a big deal as a repr(Rust) variant need not be contiguous, say (ala the size example). Other reprs have C++ or C-like guarantees, but they don't line up in the tuple case, since all anonymous tuples are repr(Rust). So maybe add "ad-hoc anonymous tuple types" to the list if things went that way. (Also not holding my breath something like this would be added just for non-repr(Rust) enums though.)

There's other use-case specific possibilities like some coordinated union and enum with unsafe, or a C-like enum as a sibling to your data if all your field types match, etc.


match f {
    obj @ A => { obj.0; obj.1; },
    obj @ B => { obj.x; obj.y; },
}

The type of a variant is the type of the enum, and enum's aren't unions, so that won't work.

In my view, I think it's reasonable to discuss whether something like

pub enum Foo {
    A(u32, f32),
    B { x: u32, y: f32 },
}
use Foo::*;

let f: Foo = todo!();
match f {
    A { .. } => {
        let x = f.0;
    }
    B { .. } => {
        let x = f.x;
    }
}

or at least

pub enum Foo {
    A(u32, f32),
    B { x: u32, y: f32 },
}
use Foo::*;

let f: Foo = todo!();
match f {
    g @ A { .. } => {
        let x = g.0;
    }
    g @ B { .. } => {
        let x = g.x;
    }
}

could become legal Rust code. The logic would be that the compiler could know statically that the variable in question is a particular enum variant at a particular place in the code1 and allow field access expressions accordingly; somewhat similarly to e.g. how variable access already is only allowed when a variable is known to be initialized. This would be a somewhat breaking change, but one that seems possible over an edition: Currently an enum can dereference to something that has fields.

1 specifically, e.g. inside of a match or an if-let that ensures the variable is of the variant in question, and when there isn't any mutable access to that variable in-between

1 Like

Great, now I came up with a "wonderful" hack that allows code like this to run successfully:

pub enum Foo {
    A(u32, f32),
    B { x: u32, y: f32 },
}
use Foo::*;

fn main() {
    let f: Foo = A(1, 2.0);
    match f {
        A { .. } => {
            let x: &u32 = &f.0;
            assert_eq!(*x, 1);
        }
        B { .. } => {
            let x: &u32 = &f.x;
        }
    }
    let f: Foo = A(42, 2.0);
    match f {
        A { .. } => {
            let x: &u32 = &f.0;
            let y: &f32 = &f.1;
            assert_eq!(*x, 42);
            assert_eq!(*y, 2.0);
        }
        B { .. } => {
            let x: &u32 = &f.x;
        }
    }
    let f: Foo = B { x: 1, y: 2.0 };
    match f {
        A { .. } => {
            let x: &u32 = &f.0;
        }
        B { .. } => {
            let x: &u32 = &f.x;
            assert_eq!(*x, 1);
        }
    }
    let f: Foo = B { x: 42, y: 2.0 };
    match f {
        A { .. } => {
            let x: &u32 = &f.0;
        }
        B { .. } => {
            let x: &u32 = &f.x;
            let y: &f32 = &f.y;
            assert_eq!(*x, 42);
            assert_eq!(*y, 2.0);
        }
    }
}

See how it's done in this playground.

Upon the first read I thought that was basically the view struct idea I briefly referenced. But I guess you mean that the binding inside the arm still has the type of the enum (and not a view struct), just with the ability to access variant fields. And I guess the automatic fix is s/ident.f/(*ident).f/g or so.

I never really thought of it before, but I guess adding a public field can cause surprising semantic breakage today, with the right Deref implementation to go along with it. Playing around.

:rofl: Guess there is a way given no more than one tuple variant and unique fields everwhere else...

1 Like

Great job identifying the shortcomings. Actually, it's even okay if multiple variants share field names as long as fields of the same name also have the same type :smiley:

1 Like

Oh yeah, just more match arms.

Understatement for monstrosity in the playground link. :slight_smile:

1 Like

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.