Ergonomics initiative discussion: Allowing owned values where references are expected

I don't think so; those traits are more restrictive, and generally apply only to slice/owned views of types, where you expect equality, hashing, etc to be the same. AsRef, by contrast, just says that you can view one reference type as another.

What led you to think that Borrow would be better?

Can you elaborate? What footguns do you see? And what's the relation to the match proposal?

A bit of background: Rust makes a distinction between coercion and subtyping. Coercions only apply at the "top level" of a type -- so you can go from &&T to &T, but not Vec<&&T> to Vec<&T>. Subtyping comes in with lifetimes, and triggers at all levels: you can go from &[&'static T] to &[&'a T].

Now, consider what it would take to allow coercions to apply deeply; in a case like Vec<&&T> above, you would actually have to create a new vector, because you're changing the elements in a significant way. Even if we could give you the tools to tell Rust how to do this, it's not the kind of thing we would generally want to happen as part of an implicit coercion, I think.

However, there might be potential to apply coercions in more places than today, without applying them arbitrarily deeply. Here's how I would think of it:

  • The AsRef proposal is about adding a new coercion based on AsRef, which fits into our existing coercion story.
  • Separately, as a distinct ergonomics improvement, we should ask whether there are situations where we can apply coercions at the function level in general. It seems quite plausible -- it would effectively be sugar for writing a closure that simply calls the function (i.e., |x| f(x)), which would apply coercions.

I'd suggest starting a separate thread on that extension, and linking to it from the Ergonomics Roadmap tracker.

2 Likes

Just my own poor understanding probably. :wink:

I'd miss-understood proposal. It'd be dangerous to treat a mut X as a &mut X and continue using it after the call mutates it, but consuming an X by value so that you cannot use it later, while passing the &mut X sounds completely fine.

If anything, consuming by value to pass an &mut focuses the &mut noise onto the real mutable borrows and makes the code easier to read. :thumbsup:

1 Like

But if we're talking about passing owned values we pass a T to a function that takes an &T; without the reflexive impl we need a special case for that.

I can see the argument: because of the restrictiveness on borrow, its more conservative than using AsRef - you can pass strings as string slices, but not as paths. However I don't think this is an upside - because Borrow's restrictions are entirely unenforced, if we do this we encourage violating those restrictions & potentially lead to people with broken hashmaps.

4 Likes

I'm still really confused; but I think most of all I'm looking for an explanation about why my request is not part of completing the the initiative Allowing owned values where references are expected, which is the feeling that I get from phrases like "starting a separate thread", "it's a separate thing", "a separate feature". My guess is that I do not understand the true scope of the initiative.

  • If my request falls under the initiative, but is impossible to fix, I'd like to know that.
  • If my request falls under the initiative, but we don't care to address it (at least this year), I'd like to know that.
  • If my request has nothing to do with the initiative as worded, I'd like to understand why not. I'd then like to help rephrase the initiative to help avoid anyone else from being confused in the same way, and to help set expectations for the eventual feature.

With that request made, I'll attempt to restate my position to reduce any confusion I might have introduced. Using the previous code, this is my understanding of the state of the world with the initial proposal.

free function

struct Thing;

fn foo(_: &Thing) {}

fn currently_does_not_work_but_would_with_the_proposal(thing: Thing) {
    foo(thing);
}

fn currently_does_not_work_but_would_with_the_proposal_also(thing: Thing) {
    Some(thing).map(|t| foo(t));
}

fn would_not_work_even_with_the_proposal(thing: Thing) {
    Some(thing).map(foo);
}

We will be able to transfer ownership of objects to methods expecting references.

method

struct Thing;

impl Thing {
    fn foo(&self) {}
}

fn currently_works(thing: Thing) {
    thing.foo();
}

fn currently_works_also(thing: Thing) {
    Some(thing).map(|t| t.foo());
}

fn currently_does_not_work_but_would_with_the_proposal(thing: Thing) {
    Some(thing).map(|t| Thing::foo(t));
}

fn would_not_work_even_with_the_proposal(thing: Thing) {
    Some(thing).map(Thing::foo);
}

This is a place where an owned value is already allowed to act as if it is a reference (map(|t| t.foo()), but not when using a different syntax that appears equivalent (map(Thing::foo)). The proposal will make it even closer (map(|t| Thing::foo(t))) which means the fact that the non-functional syntax is even harder to explain.

2 Likes

So, I think you've got a really good idea here, and I'm pretty strongly in favor of some kind of "implicit wrapper functions" that will coerce their arguments. Nonetheless, I too would prefer to see it discussed in its own thread. Not because I think there's no thematic fit, but because it is a distinct sort of coercion, and i'd rather have the space to see it discussed in full.

I think @aturon's point was that this idea -- which, iiuc, is basically that if you have some func that implements Fn(&U), we might implicitly generate a wrapper fn that implements Fn(T) where T: AsRef<U> and have this wrapper call the original -- is actually even more broadly applicable than "allowing owned values where references are expected". That is, it can be used for that, but we could also generate wrappers performing other, unrelated sorts of coercions (e.g., something this reminds me of was the idea that you could have one Rust fn (fn foo()) automatically generate wrappers with different ABIs, so that you don't need to manually write extern "C" fn foo() { ... }).

Anyway, I'd like to point out that Discuss has this awesome "spin off a related thread" button that is pretty much exactly meant for this purpose -- and I'd be pushing it right now, but that it's time for me to close the computer over here and go to bed. So maybe tomorrow. =)

3 Likes

Done, thread is here. (For reference, the bottom is under the "chain" icon, which has a "+ New Topic" section.)

I think it's worth spelling out what's going on here. In my view, it's part of the natural transition from "problem orientation" to "solution orientation".

On the one hand, you're absolutely right: solving the problem you mention is very much in scope, and should be part of the overall initiative.

On the other hand, when it comes to design, there's a danger of "scenario solving": designing a feature too specifically to a particular use case. That can lead to global designs that don't scale well, or have lots of inconsistencies.

So, when we set out to solve a problem like "allowing owned values where references are expected", we need to simultaneously keep our eye on the prize -- the full set of problems we want to solve -- and yet make sure the design we do fits into the broader context of the language.

What people have realized on this thread is that we can:

  1. Extend the applicability of coercions, so that they are applied when working with functions or closures as well.
  2. Extend the set of coercions to include AsRef.

These two things together solve the original problem. But two separate pieces of design are involved -- and in particular, the key is that we want (1) to be applicable to all coercions, not just AsRef coercions. We set out to solve one particular, narrow problem, but found that part of our solution can actually apply to a much broader range of problems, i.e. to all kinds of coercions that you want to apply at the function/closure level.

Of course, in the end we do need to make sure that we land designs for (1) and (2) such that we solve the original problem; they're not entirely orthogonal. And that's just how the design process goes: from local to global and back, iterating until you get somewhere that solves a satisfactory set of specific problems while extending the language in a globally coherent way.

3 Likes

this is why the point-free style is not popular:

https://bitbucket.org/iopq/fizzbuzz-in-rust/src/d4638b2ba3f09efe053ee0c4251036607847987c/src/lib.rs?at=master&fileviewer=file-view-default

figuring out how to write the apply function properly was impossible for me, and took a week of sitting on IRC to figure out that I cannot write it as two functions (real_deref and apply)

writing apply_0 to make the last line point-free was a waste of time, and syntax so I took it out

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.