Pattern matching on a Vec

This is currently legal in Rust:

fn main() {
    #[derive(Clone, Debug)]
    struct Int(u8);
    let foo = vec![Int(10),Int(20),Int(30),Int(40)];
    
    match &*foo { // Note the deref and reborrow here to obtain a slice
        [] => println!("empty vec"),
        [e] => println!("1 elt: {:?}", e),
        es => println!("vec: {:?}", es),
    }
}

However, if I change the match to no longer deref-into-a-slice-and-reborrow, rustc complains:

fn main() {
    #[derive(Clone, Debug)]
    struct Int(u8);
    let foo = vec![Int(10),Int(20),Int(30),Int(40)];
    
    match foo {
        [] => println!("empty vec"),
        [e] => println!("1 elt: {:?}", e),
        es => println!("vec: {:?}", es),
    }
}
  Compiling playground v0.0.1 (/playground)
error[E0529]: expected an array or slice, found `std::vec::Vec<main::Int>`
 --> src/main.rs:7:9
  |
7 |         [] => println!("empty vec"),
  |         ^^ pattern cannot match with input type `std::vec::Vec<main::Int>`

error[E0529]: expected an array or slice, found `std::vec::Vec<main::Int>`
 --> src/main.rs:8:9
  |
8 |         [e] => println!("1 elt: {:?}", e),
  |         ^^^ pattern cannot match with input type `std::vec::Vec<main::Int>`

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0529`.
error: could not compile `playground`.

To learn more, run the command again with --verbose.

So currently I'm just wondering whether there is, or will ever be, a way to destructure datastructure values e.g. a Vec with the same ergonomics as matching on slices?

Currently it seems like a weird cornercase, because you can destructure user-defined types in a match so that you can obtain ownership of data contained within, and you can destructure slices, but you cannot destructure owned versions of slices (e.g. a Vec) in a match so that you can obtain ownership of the data contained within.

EDIT: as a clarification, I'd like slice-like pattern matching on datastructure instances like Vec similarly to the current slice pattern match syntax i.e. I can access the elements contained within, but matching the elements by value rather than by reference (as is the case with slice pattern matching).

1 Like

Am I being blind or are the first 2 snippets identical?

2 Likes

They are identical, the linked playground where the error messages actually come from is different.

You may find it more readable to use vec.as_slice().

you can destructure user-defined types

Only if it does not have private fields.

To the compiler, Vec is just a struct, so matching has to use a struct pattern. You can do this, but you have to use .. to skip private fields (which is all of them).

match foo {
    Vec { .. } => println!("any Vec"),
}
1 Like

@drmason13 @steffahn yeah they were identical by mistake. Thanks for pointing it out, I've fixed that.

@cuviper what I meant in my OP is that I'd like to do slice-like pattern matching to get at the various elements contained within, but by value rather than by reference (as is the case with slices). Matching on it as a struct won't accomplish that, especially since all its fields are private.

Matching items in the Vec by value implies moving them out (unless they're Copy), which would require invoking user/library code to deal with that. Pattern matching is all structural, handled by the compiler alone.

There is one special case for box patterns moving out of a Box, but this is still an unstable feature. I think then you could convert your Vec<T> to a Box<[T]> and match box [a], box [a, b], etc.

I figured that, yeah. So currently what I'm asking is effectively not possible.

However, what if there was a trait that allows for exactly this use case?
It would define a couple of methods, e.g.

trait StructuralOwnedSliceMatch { // to be bikeshed
  type Element;
  
  fn first(self) -> Element;

  fn last(self) -> Element;

  // more methods
}

This would guarantee the value on which is matched was indeed consumed, would bind the corresponding elements to names or patterns for further matching, and could provide similar advantages to the Iterator trait in terms of ecosystem <-> language integration.

It seems you still can't move out of boxed slices either: (playground)

error[E0508]: cannot move out of type `[main::Int]`, a non-copy slice

I don't know if that's something that could eventually change under box [..] in particular, or if it's a fundamental limitation.

I think this would be resisted, just like we don't allow custom code in move or Copy.

I think a better comparison than move or Copy is the for loop syntax sugar. The way StructuralOwnedSliceMatch would integrate with match would be a lot like the way the Iterator trait interacts with for loops.

So given that an exception already exists, and the main reason was ergonomics (which also holds here), why the resistance?

IMO, pattern matching is much more fundamental than for-loop sugar.

It's not possible to move items out of Vec:

let moved = vec[i];

and this limitation (apart from lack IndexMove) comes from Drop being unable to deal with this. If you moved a few random elements out of a Vec, drop wouldn't know which items should run destructors and which shouldn't. Vec doesn't have space to keep track of such information.

match vec {
  [x, _, y, _, z] => {},
  _ => {},
}
drop(vec); // ????

If there was a trait for this, it'd need to handle multiple removals, something like vec.retain().

In theory, you could set up a "DerefMove" to a slice with a bit of finagling, so long as you only match to move fixed size array, anyway.

The deref would set the len to zero, return a pointer to the initialized portion of the slice, and the caller would be expected to read out (or drop) each element.

This is, however, not all that compatible with the Sized "DerefMove", so would have to be its own functionality specialized for slices, and that probably makes it much harder to justify (we still don't have DerefMove, even internally to the compiler, iirc).

@kornel a possible solution to that is an analogue to the IntoInterator trait, which takes ownership of all elements, and then binds them according to provided subpatterns and identifiers assuming that the pattern as a whole matches. The elements that end up bound are used, the rest is dropped immediately. The elements that are abound but otherwise unused are treated like unused locals.

@CAD97 I agree that that particular avenue seems like a lot of added complexity for rather limited upside.

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.