Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

take on bool #3189

Closed
wants to merge 9 commits into from
Closed

take on bool #3189

wants to merge 9 commits into from

Conversation

sanbox-irl
Copy link

@sanbox-irl sanbox-irl commented Oct 29, 2021

This RFC proposes adding a method called take on bool, which will return the value of the bool while also setting bool to false, similar to Option::take.

Rendered

@kpreid
Copy link
Contributor

kpreid commented Oct 29, 2021

Prior art: AtomicBool::compare_exchange, which is used for similar purposes. It is a more complex operation, because it allows specifying memory orderings (irrelevant here) and because it can set the boolean to either true or false, or act effectively as a read without modifying. But, it is closely related in that, for example, code being migrated towards or away from Sync support might replace one with the other.

Probably it doesn't make sense to name this operation bool::compare_exchange, though, since that would be misleading to users familiar with atomics and reduce the parallel to Option::take and mem::take. But I think it's worth mentioning.

let val = *self;
// and reset ourselves to false. If we are already false,
// then this doesn't matter.
*self = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should perhaps note that using core::mem::take does not actually optimize quite as well. It has an extra instruction compared:

         mov     al, byte ptr [rdi]
+        and     al, 1
         mov     byte ptr [rdi], 0
         ret

(Godbolt)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup -- I didn't want to get too in the weed here, but that's a great point. It mostly goes away once you call the method though

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not a reason to add this method, though, because it's a problem with every single two-variant enum, not just bool: https://rust.godbolt.org/z/1Wjs1bf3Y.

Its root cause is LLVM discarding information about the possible values of the discriminant. rust-lang/rust#85133 (comment) This "just" needs to get fixed, since it's actually hurting substantial amounts of Option- and Result-using code too.


# Guide-level explanation

All `.take` does is return the value of a boolean while also setting that value internally to `false`. It is exactly similar to `Option::take`, except that, of course, it only returns `true/false` instead of some inner value. (In this sense, this `.take` is effectively the same as `Option::take().is_some()`). In any example where flags are commonly read while also being reset, this method is useful.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison to Option::take is a little confusing. A more direct comparison would be to mem::take:

Something like:

All .take() does is return the value of a boolean while also setting that value internally to false. It is just like mem::take, except it is called as a method instead of a free function. In places where booleans are commonly read and then reset, like dirty flags, this method is useful.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great point. I'll make this edit.

@ehuss ehuss added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Oct 30, 2021
@joshtriplett
Copy link
Member

Micro-optimization aside, is there a reason to want this rather than mem::take?

A similar method could appear on any type that implements Default, but I don't think we should add such a method to every such type.

@jhpratt
Copy link
Member

jhpratt commented Nov 4, 2021

mem::take can likely be improved internally, no?

@jdahlstrom
Copy link

jdahlstrom commented Nov 4, 2021

A similar method could appear on any type that implements Default, but I don't think we should add such a method to every such type.

Hmm, actually, maybe this could be a provided method on Default? A known problem with the mem:: functions are that they're not very discoverable and perhaps feel like second-class citizens compared to methods. It occurs to me that to C++ programmers .take() would feel familiar, "like C++ move semantics, leaving the source object in a valid 'default' state".

@jhpratt
Copy link
Member

jhpratt commented Nov 4, 2021

Given that Default is in the prelude, I'd be strongly opposed to that.

@Havvy
Copy link
Contributor

Havvy commented Nov 4, 2021

If adding it to Default is too hard, it could be done via an extension trait trait DefaultTake {} impl<T: Default> DefaultTake for T {}

@sanbox-irl
Copy link
Author

Micro-optimization aside, is there a reason to want this rather than mem::take?

Yes, for two reasons:

  1. Adding mem into a project implies a more complex operation than .take. mem::take, imo, is used primarily in patterns where you don't want to do the borrowchecker dance -- principally, when the type isn't Copy like bool is, and so actually taking ownership and replacing it with something else requires non-trivial unsafe code. In general, the mem module has a collection of relatively low-level operations on memory patterns, and mentally, I, and I suspect many Rust programmers, go to it when we're doing memory manipulation. As @jdahlstrom put it, it's a bit of a "second-class citizen compared to methods". I believe there are two patterns where a "Take" pattern is used that is more about API usability, rather than memory handling: Options and bool. For the former, we already have the .take method (and in my Rust experience, anecdotally, is used often). We don't have anything for the latter.

  2. It's a common enough operation that importing the use declaration can be annoying enough to justify its existence.

As a general statement here though, I suspect this PR divides users cleanly into "I have always wanted this and wished it existed" and "I have never wanted that, and I think it's bloat."

@sanbox-irl
Copy link
Author

Also updated with feedback from @kpreid and @mbartlett21, thank you both!

@jquesada2016
Copy link

Adding take() for all T where T: Default is a splendid idea!

@mbartlett21
Copy link
Contributor

  1. Adding mem into a project implies a more complex operation than .take. mem::take, imo, is used primarily in patterns where you don't want to do the borrowchecker dance -- principally, when the type isn't Copy like bool is, and so actually taking ownership and replacing it with something else requires non-trivial unsafe code. In general, the mem module has a collection of relatively low-level operations on memory patterns, and mentally, I, and I suspect many Rust programmers, go to it when we're doing memory manipulation. As @jdahlstrom put it, it's a bit of a "second-class citizen compared to methods". I believe there are two patterns where a "Take" pattern is used that is more about API usability, rather than memory handling: Options and bool. For the former, we already have the .take method (and in my Rust experience, anecdotally, is used often). We don't have anything for the latter.

Perhaps you could put this under the Motivation section, saying why mem::take, though it has the same functionality, is not as suitable for the task?

@sanbox-irl
Copy link
Author

  1. Adding mem into a project implies a more complex operation than .take. mem::take, imo, is used primarily in patterns where you don't want to do the borrowchecker dance -- principally, when the type isn't Copy like bool is, and so actually taking ownership and replacing it with something else requires non-trivial unsafe code. In general, the mem module has a collection of relatively low-level operations on memory patterns, and mentally, I, and I suspect many Rust programmers, go to it when we're doing memory manipulation. As @jdahlstrom put it, it's a bit of a "second-class citizen compared to methods". I believe there are two patterns where a "Take" pattern is used that is more about API usability, rather than memory handling: Options and bool. For the former, we already have the .take method (and in my Rust experience, anecdotally, is used often). We don't have anything for the latter.

Perhaps you could put this under the Motivation section, saying why mem::take, though it has the same functionality, is not as suitable for the task?

thank you!

@programmerjake
Copy link
Member

adding a replace method on bool would also be a good idea, since I mentally put take and replace in the same category.

@tanriol
Copy link

tanriol commented Nov 10, 2021

To me a take feels out of place on a bool just like it would look weird on an u32, f64 or Duration. All these types have a Default impl, but none of them have that default strong enough to justify a dedicated "copy and reset to default" operation.

Depending on the scale of the problem, in my code I'd use one of:

  • mem::replace and/or mem::take
  • Option<()>
  • struct Flag(bool) with methods set, reset and take

and I'd prefer any of these to bool::take.

@scottmcm
Copy link
Member

➕💯 to @tanriol's post here. I agree that most Default types shouldn't have take, and that mem::take is perfect for the comparatively-rare situations where it's what's needed.

That said, my opposition here is basically entirely to the precedent from naming this take.

If this was instead phrased as bool-terminology methods, then I'm more in the "sure, why not" camp. Perhaps this:

impl bool {
    #[must_use = "If you don't need the return value, just use `= false`"]
    pub reset(&mut self) -> bool {
        mem::replace(self, false)
    }

    #[must_use = "If you don't need the return value, just use `= true`"]
    pub set(&mut self) -> bool {
        mem::replace(self, true)
    }
}

/*
// without this RFC, our code would be slightly uglier like this:
if self.dirty {
self.dirty = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a reader, I prefer this version. It is only one line longer and it is much clearer what is going on. take is not immediately obvious unless you are very familiar with the option version, and even then I think you might stop for a minute to consider the semantics (looking just at self.dirty.take() I might assume dirty is being treated as some kind of pointer and left as null).

(I also have a preference against functions which have both an important side effect and return an important value, they tend to be 'too clever' and to surprise readers).

@joshtriplett
Copy link
Member

@rfcbot close

@rfcbot
Copy link
Collaborator

rfcbot commented Feb 25, 2023

Team member @joshtriplett has proposed to close this. The next step is review by the rest of the tagged team members:

No concerns currently listed.

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. disposition-close This RFC is in PFCP or FCP with a disposition to close it. final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. and removed proposed-final-comment-period Currently awaiting signoff of all team members in order to enter the final comment period. labels Feb 25, 2023
@rfcbot
Copy link
Collaborator

rfcbot commented Feb 28, 2023

🔔 This is now entering its final comment period, as per the review above. 🔔

@rfcbot rfcbot added finished-final-comment-period The final comment period is finished for this RFC. to-announce and removed final-comment-period Will be merged/postponed/closed in ~10 calendar days unless new substational objections are raised. labels Mar 10, 2023
@rfcbot
Copy link
Collaborator

rfcbot commented Mar 10, 2023

The final comment period, with a disposition to close, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

This is now closed.

@rfcbot rfcbot added closed This FCP has been closed (as opposed to postponed) and removed disposition-close This RFC is in PFCP or FCP with a disposition to close it. labels Mar 10, 2023
@rfcbot rfcbot closed this Mar 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed This FCP has been closed (as opposed to postponed) finished-final-comment-period The final comment period is finished for this RFC. T-libs-api Relevant to the library API team, which will review and decide on the RFC. to-announce
Projects
None yet
Development

Successfully merging this pull request may close these issues.