Unwrap a `Vec` if it contains exactly 1 element

I try to avoid .unwrap() where I can. One situation where I can't avoid it is this:

let mut items: Vec<SomeType> = ...;
if items.len() == 1 {
    items.pop().unwrap()
} else {
    ...
}

This comes up quite a few times in my code. The following would be nicer, but it doesn't work if SomeType is not Copy:

if let &[item] = &items[..] {
    item
} else {
    ...
}

A try_into_single method on Vec would solve this:

items.try_into_single().unwrap_or_else(|items| ...)
Implementation
// in impl<T, A: Allocator> Vec<T, A>
fn try_into_single(mut self) -> Result<T, Vec<T, A>> {
    if self.len() == 1 {
        // SAFETY: We checked that the Vec is not empty, so this always returns Some(_)
        Ok(unsafe { self.pop().unwrap_unchecked() })
    } else {
        Err(self)
    }
}

Would other people find this useful in the standard library as well?

It’s a bit clunky, but you can try_into an array of one element:

if let Ok([item]) = <[_; 1]>::try_from(items) {
15 Likes

Thanks, this is what I was looking for!

I'd need a match or unwrap_or_else instead of if let so I can use items in the 'else' branch. Luckily the TryFrom impl returns the original Vec in the Err variant.

5 Likes

You can also try_into a slice to a reference to an array if you don’t want to consume the vector.

1 Like
5 Likes

Note that the OP example wants to keep the Vec in the else case, whereas the itertools option results in roughly (Option<(T, Option<T>)>, vec::Iter<T>). If all you need is some impl Iterator<Item=T> in the else case, then this is fine, but there's no way to recover the original vector other then collecting to a new one. (And unlike simple map/collect code, there isn't a specialization to reuse the existing vector.)

2 Likes

If the let chains finally land the fairly straightforward version should work:

if vec.len() == 1 && let Some(item) = vec.pop() {
} 
else { 
}
7 Likes

Apart from the valid solutions suggested here, your statement, that this comes up a few times in your code may suggest a design flaw.

Instead of using Vec<SomeType>, maybe you want to use something like

pub enum OneOrMany<T> {
    One(T),
    Many(Vec<T>),
}

instead.

The project I was talking about is a language, written in Rust. Its AST contains lists everywhere. For example:

enum Node {
    Literal(Literal),          // "abc"
    Group(Group),              // (a b c)
    Alternation(Alternation),  // a | b | c
    // ...omitted
}

struct Group {
    span: Span,
    children: Vec<Node>,
}

struct Alternation {
    span: Span,
    children: Vec<Node>
}
// ...etc

When transforming the AST to a different representation, there's often an optimization possible when a node contains only a single element. But using a OneOrMany enum for them is not feasible, as that would make the parser more complicated, and also interfere with other, unrelated code.

I don't think that's necessarily true. You could make OneOrMany have essentially the same interface as Vec if you wanted to.

I don't see anything wrong with your original code. I think you misunderstand the purpose and potential drawbacks of unwrap(). Please read Andrew Gallant's great article on unwrap().

Not OP, but I regularly write Rust code in a context where any panic would be very bad. So any code that has to call a method that panics in a case which should be impossible (such as .unwrap() or .expect(..) on an option that should always be Some), has to come with a thorough justification of why the panicking case is impossible, which needs to be addressed each time the code is updated. Using an alternative method like this which allows an implementation that avoids potentially-panicking methods is a strict upgrade.

For cases where panicking on a bug in the program is acceptable, he makes a reasonable argument for .unwrap() being a potentially-idiomatic call, but not all cases have panicking as an acceptable option.

The OP has the code

if items.len() == 1 {
    items.pop().unwrap()
} else { ... }

Which is reasonably self justifying. In rust I prefer the form if let Ok([item]) = <[_; 1]>::try_from(items) {, but this has more to do with moving the exhaustiveness check to compile time, especially since this pattern is repeated in many places in OP's codebase. In OP's place, prior to consulting this thread, I would probably have gone with a helper function.