2

Please consider the following program (it's a minimized example, so don't get hung up on the fact what I'm trying to do here could be better achieved with a HashMap<u32, Vec<MyVariant>>):

#[derive(PartialEq)]
enum MyVariant {
    None,
    SomeValue,
    SomeOtherValue,
    Aggregate(Vec<MyVariant>)
}

impl MyVariant {
    pub fn with(self, other: Self) -> Self {
        if self == Self::None {
            other
        } else if other == Self::None {
            self
        } else {
            Self::Aggregate(vec![self, other])
        }
    }
}

fn add_variant(vec: &mut Vec<(u32, MyVariant)>, id: u32, variant: MyVariant) {
    if let Some(item) = vec.iter_mut().find(|item| item.0 == id) {
        *item = (id, item.1.with(variant));
    } else {
        vec.push((id, variant));
    }
}

fn main() {
    let mut vec: Vec<(u32, MyVariant)> = Vec::new();
    add_variant(&mut vec, 1, MyVariant::SomeValue);
    add_variant(&mut vec, 2, MyVariant::None);
    add_variant(&mut vec, 1, MyVariant::SomeOtherValue);
}

This will fail at *item = (id, item.1.with(variant)), because item is a shared mutable reference, so item.1 cannot be moved into the with() method. I tried to get a reference using an index into the vector, but that doesn't solve the underlying problem.

Basically, what I want is a method that will temporarily move a value out of a mutable vector, and replaces it with a new, derived value. Maybe something like:

impl<T> Vec<T> {
    /// Replaces the item at `index` with a new item produced by `replacer`.
    /// `replacer` receives the old item as argument.
    fn replace_with(&mut self, index: usize, replacer: Fn(T) -> T);
}

But I cannot find a method like the above, or any technique that does achieve the same intent.

I suspect I might be looking at it from the wrong angle, so any ideas? Thanks!

arendjr
  • 679
  • 6
  • 12
  • How about [`std::mem::take()`](https://doc.rust-lang.org/std/mem/fn.take.html) or [`std::mem::replace()`](https://doc.rust-lang.org/std/mem/fn.replace.html)? – Sven Marnach Sep 28 '20 at 12:10
  • Well, the variant has no default (though I guess I could add that), but indeed I can solve it using two `std::mem::swap()` calls. I just wish there'd be a more ergonomic API... – arendjr Sep 28 '20 at 12:15
  • Update: As shown in Peter Hall's answer, I did settle on using `std::mem::replace()` indeed. – arendjr Sep 29 '20 at 10:27
  • 1
    I suggest accepting that answer then – the code is slightly nicer, so it's what future readers of this question should go with. – Sven Marnach Sep 29 '20 at 11:33

3 Answers3

2

Given that you cannot copy or clone variant, and that with takes ownership, you will need to put a dummy value were it was, to avoid the presence of unitialized memory. You can do that with std::mem::replace:

fn add_variant(vec: &mut Vec<(u32, MyVariant)>, id: u32, variant: MyVariant) {
    if let Some(item) = vec.iter_mut().find(|item| item.0 == id) {
        let old_item = std::mem::replace(&mut item.1, MyVariant::None);
        item.1 = old_item.with(variant);
    } else {
        vec.push((id, variant));
    }
}
Peter Hall
  • 53,120
  • 14
  • 139
  • 204
1

This code should work. It differs from yours by constructing a temporary placeholder, then swapping it into the Vec using std::mem::swap so that the real value can be passed by value.

#[derive(PartialEq)]
enum MyVariant {
    None,
    SomeValue,
    SomeOtherValue,
    Aggregate(Vec<MyVariant>)
}

impl MyVariant {
    pub fn with(self, other: Self) -> Self {
        if self == Self::None {
            other
        } else if other == Self::None {
            self
        } else {
            Self::Aggregate(vec![self, other])
        }
    }
}

fn add_variant(vec: &mut Vec<(u32, MyVariant)>, id: u32, variant: MyVariant) {
    if let Some(item) = vec.iter_mut().find(|item| item.0 == id) {
        let mut temp = (0, MyVariant::None); // create placeholder value
        std::mem::swap(item, &mut temp); // swap the placeholder in
        *item = (id, temp.1.with(variant)); // overwrite with real value
    } else {
        vec.push((id, variant));
    }
}

fn main() {
    let mut vec: Vec<(u32, MyVariant)> = Vec::new();
    add_variant(&mut vec, 1, MyVariant::SomeValue);
    add_variant(&mut vec, 2, MyVariant::None);
    add_variant(&mut vec, 1, MyVariant::SomeOtherValue);
}
apetranzilla
  • 5,331
  • 27
  • 34
  • Thanks! The work-around I was trying just now used two swaps, but this is indeed better. Still not as ergonomic as I would've hoped, but I guess it'll do :) – arendjr Sep 28 '20 at 12:18
  • Unfortunately this is just a limitation of how Rust works - you're not allowed to have an "empty" spot in the vector, and so there's no safe way to temporarily take a value out to pass it with move semantics aside from having a temporary value to swap in first. You might be able to make the code a little bit nicer by adding a `Default` implementation and using `std::mem::take` or writing a helper function. – apetranzilla Sep 28 '20 at 12:20
0

There are good options in apetranzilla's answer, but you can make the replacement in your struct implementation itself:

impl MyVariant {
    pub fn with_inplace(&mut self, other: Self) {
        if self == &Self::None {
            *self = other
        } else {
            if other != Self::None {
                let tmp = std::mem::replace(self, Self::None);
                *self = Self::Aggregate(vec![tmp, other]);
            }
        }
    }
}

Then call it in iteration:

if let Some(item) = vec.iter_mut().find(|item| item.0 == id) {
    item.1.with_inplace(variant);
} else {
    vec.push((id, variant));
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366