Add `std::mem::{swap, replace, take}` to the prelude

I find myself using std::mem::{swap, replace, take} frequently. They're essential parts of the language that (imo) could use more visibility and deserve to be exported somewhere that's less obscure and quicker to name. I'd guess that adding them to the prelude would cause name collision/clarity issues, although I'd still support it personally. Alternatively, and less disruptively, they could be re-exported directly under std and core -- does that appeal to people?

[Edit: It's been pointed out that name collisions aren't in fact an issue here. So my preference is to just add these functions to the prelude.]

5 Likes

Maybe as a less destructive means we could add use std::mem; to the prelude in the 2021 edition.

That way they are accessible as mem::swap, ... which I think is better than having all three functions.

19 Likes

That effectively adds mem as a new top-level namespace, which seems more disruptive than re-exporting some functions under std. And not hiding swap and co. inside mem (which is kind of an obscure location imo) was a significant motivation for my original suggestion.

I think it's ok, actually, since used items are preferred over prelude ones. (The problem is trait methods, which don't have this safety net, and thus cause ambiguity.)

I agree that replace is a quintessential Rust method, and would support having them in the prelude.

(I think that drop being there gives a potential precedent argument.)

I'm not a fan of that. Right now it's common to call them as mem::replace (with the use std::mem;) -- I think if we're going to encourage a convention change it should be all the way to replace, not to std::replace.

7 Likes

My worry was that swap/replace/take in the prelude might collide with existing user-defined functions, not with the same functions imported from their canonical paths. But I'd be delighted if that's not a major concern after all.

Huh, I wasn't aware of that practice. use std::mem; has never appealed to me, because I don't like calling the functions as mem::replace etc., but apparently I'm in the minority.

1 Like

Given that use std::mem; is already common. And that I don't think that namespaced functions can conflict with trait functions would adding it to the prelude be a backwards compatible change?

If somebody's code defines a free function named replace then adding std::mem::replace to the prelude will break that module (right?). I don't know whether that's a problem in practice but it certainly could occur. Never mind, I just tried this on the playground with drop and there's no breakage -- TIL! Okay, in that case I support just adding these to the prelude in the 2021 edition.

I guess I should clarify that I only know this anecdotally. It might be a property of the codebases I look at, not something common.

But here's part of a rg '[^.a-zA-Z]replace\(' --type rust in the rust repro, as an example:

Since I was also just doing it, here's a demo for anyone who wants to try at home:

mod uhoh {
    pub fn drop(x: i32) {
        dbg!(("yup, not the prelude one", x));
    }
}
use uhoh::drop;

fn main() {
    drop(4);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=71f2b2c819ab9d47d29e88c04be058d5

4 Likes

Purely anecdotal, but the first time I ended up using swap for some sentinel pattern situation, I felt the need to go ask IRC if I was doing something bad or silly. Some combination of not having seen the pattern yet and it being not imported while living beside things like transmute and forget.

5 Likes

I agree that these functions living in mem makes them "feel" scary. I don't think putting them in the prelude is the right way to demystify them either.

I almost always use mem::swap and other free functions by importing the namespace, not the free fun function. Personally, I do this for basically every free function, even my own. (I'm also writing C++ professionally for $gradschool right now, so maybe that's (anti)corroded my style preferences...)

If they were in the prelude, I'd get used to using them unprefixed. But I think the "better" solution is to address what makes the mem module "scary" in the first place. One of the most empowering things that Rust gives you is the ability to do things terrifying in less safe languages without worry, because tyck and borrowck have your back.

12 Likes

Given that I don't even like drop being in the prelude, I very much would not want this. I want to see where free functions come from, as it's not always immediately obvious what the context is.

2 Likes

De-mystifying std::mem sounds good to me, but there are also other other motivations for having swap etc. in the prelude:

  • they're useful in a variety of situations, not just when doing detailed stuff with memory
  • they should be prominently visible and easy to discover, because they solve a problem that comes up pretty regularly when writing Rust code and that can't readily be worked around
  • typing out std::mem::swap (or adding an import mem; if you incline to that style) adds friction that I think should not exist here

I can sympathize with not liking free functions without a namespace. On the other hand, adding swap etc. to the prelude doesn't stop you from spelling out the full paths or importing std::mem in your code if you prefer that. (I can even imagine a Clippy lint for banning calls to free functions outside the current module without a namespace.)

I definitely agree that std::mem::replace is one of Rust's best-kept secrets, and that it should be more discoverable. I'm not sure how much the prelude would help with that goal, but it might.

As an aside: If I were designing a Rust-like language from scratch today, instead of an assignment operator I would include an infix operator like a <- b that is equivalent to replace(&mut a, b), and perhaps a <-> b to mean swap(&mut a, &mut b).

22 Likes

Can you elaborate on this?

I'm curious if we could isolate what it is about free functions that cause that desire. For example, do you have the same feelings about types (like Vec)? Or trait methods (like .clone())? Would you prefer that, say, it be a SwapExt trait in the prelude so it would be a.swap(&mut b) instead of swap(&mut a, &mut b)? There's also been talk of perhaps adding HashMap to the prelude -- would that trigger the same objection?

I think this is a good summary of my instinct here. That they'd become kinda like "operators" in my head (as in Matt's aside), just alphabetic ones instead of symbolic ones. Or maybe "vocabulary words". So these shouldn't be that many of them -- I wouldn't want create_dir_all in the prelude, for example, even though the name makes it pretty clear even with an fs:: prefix.

2 Likes

Would this be the default assignment in order to facilitate linear types?

1 Like

There are a lot of functions in std::mem with generic enough names that they might as well do something else (you can replace a lot of things in many ways). Hence, the practice of calling them as mem::replace() etc. emerged, so as to indicate that they are memory management primitives and do nothing funny.

11 Likes

With the exception of drop, there aren't any free functions in the prelude (I suspected this, and a quick check confirmed it). Some traits that are applicable to nearly everything, like the associated traits for derives, From/Into, ToOwned, etc. are also in the prelude. To me, that they're applicable to just about any program someone might want to write is the reason they are present. drop, on the other hand, is not as widely applicable. I just did a quick rg "drop\(" over the directory I keep all my projects in; the only match was in standback, which is derived from stdlib. Said another way, I don't call drop anywhere at all in any of my Rust code.

Performing the same (limited) analysis for std::mem::{swap, replace, take}, there is all of one match wherein I swap a telemetry log out for an empty one. Checking for as_mut(), as_ref(), into()/::into(), into_iter(), iter(), to_owned(), to_string(), and ::from(_) yields a cumulative 607 matches (nb: not lines). This does not include fully-qualified syntax for most of the methods. For reference, these projects combine to 26,834 lines of Rust code per tokei.

Macros are a different story that really isn't applicable here, as nothing being talked about is macros.

So as I've stated previously in other threads, I suppose the reason I'm opposed to it is primarily the lack of real-world usage in the areas of the projects I have cloned locally. I also personally believe that having the mem:: prefix is preferred, but that's a style concern. I understand that other use cases would benefit from these methods being in the prelude in the same way that drop is, but ~1/27,000 is hardly comparable to ~1/44 in my real-world codebases.

Now that I am finishing typing this out, I realize that it's not really an argument against free functions being in the prelude, just that it's not used enough in my experience. But regardless, hopefully this helps somehow.

As a side note, I have excluded the rust and standback repositories that I have cloned locally from this data, as I'm fairly certain they are nowhere near representative of the average codebase.

1 Like

That's great. It answers my core question of whether there's something about functions that needs to be considered differently. (I don't know whether default() might ever be in the prelude in addition to Default, but if there was something bad about functions it would be important to know.)

The concern over insufficient usage I always find hard to weigh, because it's just so hard to find a representative sample. Totally agree that swap is way less common than something like .clone(). But it would also be easy to have FSM-based code where .to_string() never shows up but replace is common.

I would definitely expect analysis like this to be welcome in any FCP about whether or not to do this. Personally I like them for their core "ownershipness", but I'm also not on the team that would have to approve a prelude addition. (AFAIK that's libs.)

Running an analysis like this over crates.io and/or Rust GitHub repos (similar to how crater works) could potentially prove useful. For something like that, we'd probably want a tool that is actually aware of the types, such that aliases, fully-qualified calls, etc. are covered: I would imagine my trivial regex missed some edge cases.

No idea how this type of thing would be created, but it's an interesting idea that I'd help with. Any changes to the prelude beyond adding TryFrom and TryInto will almost certainly prove controversial, so having hard data could prove invaluable.

I can understand that motivation. But I see these functions as part of the core Rust "vocabulary" (to use @scottmcm's language), not specialized tools, and making them available unqualified seems like a reasonable way to reflect that in std.

1 Like