From 31b282ad9f72c54e453298a33d9610ea461e1ad2 Mon Sep 17 00:00:00 2001 From: Andre Bogus Date: Sat, 8 Oct 2016 08:51:31 +0200 Subject: [PATCH 1/2] new idiom: mem::replace --- idioms/mem-replace.md | 83 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 idioms/mem-replace.md diff --git a/idioms/mem-replace.md b/idioms/mem-replace.md new file mode 100644 index 00000000..f312174c --- /dev/null +++ b/idioms/mem-replace.md @@ -0,0 +1,83 @@ +# `mem::replace` to keep owned values in changed enums + +## Description + +Say we have a `&mut MyEnum` which has (at least) two variants, +`A { name: String, x: u8 }` and `B { name: String }`. Now we want to change +`MyEnum::A` to a `B` if `x` is zero, while keeping `MyEnum::B` intact. + +We can do this without cloning the `name`. + +## Example + +```rust +use std::mem; + +fn a_to_b(e: &mut MyEnum) { + // we mutably borrow `e` here. This precludes us from changing it directly + // as in `*e = ...`, because the borrow checker won't allow it. Of course + // we could just take a reference to `name` and clone that, but why pay an + // extra allocation for something we already have? + let have_name = match *e { + MyEnum::A(ref mut name, x) if x == 0 => { + // this takes out our `name` and put in an empty String instead + // note that empty strings don't allocate + Some(mem::replace(name, "".to_string())) + } + // nothing to do in all other cases + _ => None + }; + // the mutable borrow ends here, so we can change `e` + if let Some(name) = have_name { *e = MyEnum::B { name } } +} +``` + + +## Motivation + +When working with enums, we may want to change an enum value in place, perhaps +to another variant. This is usually done in two phases to keep the borrow +checker happy. In the first phase, we observe the existing value and look at +its parts to decide what to do next. In the second phase we may conditionally +change the value (as in the example above). + +The borrow checker won't allow us to take out `name` of the enum (because +*something* must be there. We could of course `.clone()` name and put the clone +into our `MyEnum::B`, but that would be an instance of the [Clone to satisfy +the borrow checker] antipattern. Anyway, we can avoid the extra allocation by +changing `e` with only a mutable borrow. + +`mem::replace` lets us swap out the value, replacing it with something else. In +this case, we put in an empty `String`, which does not need to allocate. As a +result, we get the original `name` *as an owned value*. We can wrap this in +an `Option` or another enum that we can destructure in the next step to put the +contained values into our mutably borrowed enum. + +## Advantages + +Look ma, no allocation! Also you may feel like Indiana Jones while doing it. + +## Disadvantages + +This gets a bit wordy. Getting it wrong repeatedly will make you hate the +borrow checker. The compiler may fail to optimize away the double store, +resulting in reduced performance as opposed to what you'd do in unsafe +languages. + +## Discussion + +This pattern is only of interest in Rust. In GC'd languages, you'd take the +reference to the value by default (and the GC would keep track of refs), and in +other low-level languages like C you'd simply alias the pointer and fix things +later. + +However, in Rust, we have to do a little more work to do this. An owned value +may only have one owner, so to take it out, we need to put something back in – +like Indiana Jones, replacing the artifact with a bag of sand. + +## See also + +This gets rid of the [Clone to satisfy the borrow checker] antipattern in a +specific case. + +[Clone to satisfy the borrow checker]() From 547ff18605f06dabed6dfa96654d91cb2044fcbe Mon Sep 17 00:00:00 2001 From: Andre Bogus Date: Sat, 8 Oct 2016 19:14:05 +0200 Subject: [PATCH 2/2] Adressed comments --- README.md | 1 + idioms/mem-replace.md | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 300ae6b9..4dbe3ba8 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ language. * [Iterating over an `Option`](idioms/option-iter.md) * TODO `Default` trait * [Pass variables to closure](idioms/pass-var-to-closure.md) +* [`mem::replace(_)` to avoid needless clones](idioms/mem-replace.md) ### Design patterns diff --git a/idioms/mem-replace.md b/idioms/mem-replace.md index f312174c..2b3359bf 100644 --- a/idioms/mem-replace.md +++ b/idioms/mem-replace.md @@ -19,7 +19,7 @@ fn a_to_b(e: &mut MyEnum) { // we could just take a reference to `name` and clone that, but why pay an // extra allocation for something we already have? let have_name = match *e { - MyEnum::A(ref mut name, x) if x == 0 => { + MyEnum::A { ref mut name, x } if x == 0 => { // this takes out our `name` and put in an empty String instead // note that empty strings don't allocate Some(mem::replace(name, "".to_string())) @@ -28,7 +28,7 @@ fn a_to_b(e: &mut MyEnum) { _ => None }; // the mutable borrow ends here, so we can change `e` - if let Some(name) = have_name { *e = MyEnum::B { name } } + if let Some(name) = have_name { *e = MyEnum::B { name: name } } } ``` @@ -80,4 +80,4 @@ like Indiana Jones, replacing the artifact with a bag of sand. This gets rid of the [Clone to satisfy the borrow checker] antipattern in a specific case. -[Clone to satisfy the borrow checker]() +[Clone to satisfy the borrow checker](TODO: Hinges on PR #23)