Smart pointer which owns its target

Or the compiler could be improved?

I would like to make a statement in particular regarding #98905:

  • I consider this A BUG (p.s.: not so sure anymore, see the end of this very very long post).
  • I am aware that it cannot be fixed easily, which is why I added: "Fixing this may be a breaking change and/or require a new edition, I guess." I assumed that this is sufficient to put my bug report into context.
  • I provided three potential solutions in the bug report:
    • In my OP: Simply changing impl<T: ?Sized + ToOwned> AsRef<T> for Cow<'_, T> into impl<T: ?Sized + ToOwned + AsRef<U>, U: ?Sized> AsRef<U> for Cow<'_, T>, which is the breaking change that I understand can't happen (but I didn't comment more on that in that issue because this is a bug report and not a pull request).
    • In a comment: Introducing a GenericCow (which might be a good idea anyway :innocent:), and then migrating from Cow to a new Cow type. (In my post, I said "potential migration path", to make clear this is me fathoming possibilities here, and not making a request to change something at this stage.)
    • In a second comment, where I applied @quinedot's ideas. This was to clarify that the assumed root cause of the problem may be somewhere else than what I assumed in my original post in that issue on GitHub. Following these thoughts, the hypothetical solution would be to deprecate AsRef and provide a new AsRef2 (as I called it for purposes of referencing). (Here, I defensively added "Please note that this is not a suggested solution but merely a thought-experiment to better understand the cause of the problem.")

When talking about "the venue", I think there's a huge difference between a bug report and a pull request. Admittingly, I named the issue:

Cow<'_, T> should not implement AsRef but AsRef where T: AsRef

But this is what "the bug" is:

  • Given the current semantics of AsRef and Borrow:
    • Cow has a wrong blanket implementation for AsRef.

Yeah, I use the strong word "wrong" here. Same as it is "wrong" that in Haskell a Monad wasn't a superclass of a Functor if we also have an Applicatative Functor (which took years to be solved and caused a lot of frustration [1], I guess, see Applicative Functor Monad Proposal).

Maybe the issue in Haskell was much worse than what I try to point out with #98905 (or worse than the issue @quinedot brought up to surface in regard to the anti-pattern misuse regarding AsRef for overcoming "unsightly" syntax), but I'm sure that in the Haskell community there were a lot of people who argued: "Is it really a big deal? Let's just leave things as they are. It's not a bug, it's like it is."

But it's not only for semantic reasons that I claim #98905 is a bug. I think the inconsistent implementation of AsRef for Cow will cause practical issues where code doesn't compile [2] where it should compile unless we fix documentation accordingly so people know that their code will not compile and why it doesn't compile.

So I think the venue was exactly right for my concern.

I think such "bugs" should be acknowledged as such. Instead, I feel like the usual way to deal with inconsistencies in std is:

:see_no_evil: :hear_no_evil: :speak_no_evil:
(plus blaming the reporter for being inconsiderate about proposing breaking changes!)

And why is that so? Maybe because there is allegedly no solution to these problems. But that doesn't mean the problems (or errors) don't exist. And what I would like to propose (and I seem to have gotten positive feedback from @CAD97 on that matter), is to not dismiss these issues but at least create a documentation fix. [3]

But why? I feel like there is an atmosphere where you have to be extremely defensive when bringing up new ideas. I posted on a similar issue before. Not everyone who is entering this community "knows" these implicit rules what posting on IRLO or the bug tracker means (edit: and how wording is going to be interpreted!). And I think these rules are not good, because they lead to important matters being dismissed where they should not be. And I believe they lead to people (particularly those who understand the semantics of complicated abstract data models) leaving the community because they are disappointed of getting negative feedback on good ideas. But that's just a wild guess based on my own feelings in that matter.

That said, I would like to add that I overall did not feel bad about @CAD97's and @quinedot's responses (with some minor exceptions maybe, but I know I'm pretty sensitive also) which were all well-founded and helped me to advance my knowledge. I am really really thankful for your help!!

It was more the overwhelming amount of negative responses overall. But that's maybe also because many people don't understand the point of the issues I try to bring up (sometimes including myself).

I understand, but I have brought up topics like that here and I was referred to IRLO. Which again brings me to what I wrote here:

So… is "Smart pointer which owns it's target" a topic for URLO or IRLO?

:man_shrugging:

I think especially the new faces should be treated with a gentle response, but that's a problem I see elsewhere in the tech-scene as well. And being treated differently just because I have made constructive contributions before is nothing I want. I hate having to "prove myself". (But maybe I'm unrealistic here, and that's something I just have to deal with.)

Phew, difficult issue. Not sure what I should say. I'm not sure a moderator could have helped me in the particular matter. Afterall, nobody really did something really wrong (I think). It's a difficult issue where nobody is to blame, I guess. That said, maybe some things could be done better though.

I would like to do that (disclaimer: if I find time). In short that would be:

  • AsRef isn't reflexive (Playground) because of:
    • historic reasons to avoid "unsightly" notation,
    • which require 'As to lift over &" (I have to learn more about category theory),
    • which is conflicting with AsRef being reflexive.
  • AsRef isn't transitive (Playground).
  • Sometimes Borrow can and should be used where this is a problem,
  • Cow's AsRef implementation:
    • is (arguably) semantically wrong (see my OP in #98905),
    • but won't be removed due to backwards compatibility.
    • Instead of .as_ref() you can use dereferencing (*) or .borrow() (of which the latter may be ambiguous in some cases).

Of course, this should be phrased in a more verbose way and garnished with examples such that beginners can understand the implications.

Edit: @CAD97 Of course, this doesn't include yet the points you suggested. I was just listing my (current) understanding of how things are, so I don't forget later. And also not sure anymore about whether Cow's AsRef implementation is semantically wrong. But at least there are inconsistencies that should be pointed out!

But that's only the positive part of the story. :see_no_evil:

I will keep that in mind and I like the way to work around it: Provide good module-level documentation and link to it in the type-/trait-level documentation.

Nice try, but…

use std::borrow::Cow;
use std::ffi::OsStr;
use std::path::Path;

fn foo(_: impl AsRef<Path>) {}
fn bar(_: impl AsRef<i32>) {}

fn main() {
    foo("Hi!");
    foo("How are you?".to_string());
    foo(&&&&&&("I'm fine.".to_string()));
    foo(&&&&&&&&&&&&&&&&&&&"Still doing well here.");
    //bar(5); // ouch!
    //bar(&5); // hmmm!?
    bar(Cow::Borrowed(&5)); // phew!
    foo(Cow::Borrowed(OsStr::new("Okay, let me help!")));
    foo(&&&&&&&&Cow::Borrowed(OsStr::new("This rocks!!")));
    //foo(&&&&&&&&Cow::Borrowed("BANG!!"));
}

(Playground)

@quinedot: Do you see how I feel like this is "buggy"?

Yeah, but also the "flaws" (see Playground above) even if talking about them might not be so "fluffy". :pensive:

:sweat_smile:

So you developed that idea thorughout our discussion and didn't had it up your sleeve? So then this knowledge is indeed something not well-known?

Well, I outlined a way. :innocent: Didn't say it wasn't painful. :smiling_imp:

(Seriously, I understand if that wasn't done because it might leave us with a far greater mess in the end, especially if there's maybe a way out one day with specialization.)

It's difficult to put in words. I'll give it a try. It's because Cow<'a, T> relates to T just like:

We cannot go from either of those two to T via .as_ref() but must use .borrow():

fn main() {
    // using `.borrow()`:
    let _: &i32 = 0i32.borrow();
    let _: &i32 = Cow::<i32>::Owned(0i32).borrow();
    let _: &i32 = Cow::Borrowed(&0i32).borrow();
    let _: &i32 = Owned(0i32).borrow();
    // using deref-coercion:
    let _: &i32 = &0i32;
    let _: &i32 = &Cow::<i32>::Owned(0i32);
    let _: &i32 = &Cow::Borrowed(&0i32);
    let _: &i32 = &Owned(0i32);
    // using `.as_ref()`:
    //let _: &i32 = 0i32.as_ref(); // Won't work
    let _: &i32 = Cow::<i32>::Owned(0i32).as_ref();
    let _: &i32 = Cow::Borrowed(&0i32).as_ref();
    //let _: &i32 = Owned(0i32).as_ref(); // Won't work
}

(Playground)

No, I don't want. Unless you mean I should want? :thinking: I simply think that .as_ref() isn't the right operation to go from a "maybe reference-like type" to a reference. It's for cheap reference-to-reference conversion (just like the docs say). It can only be used in concrete cases, e.g. to go from PathBuf to Path, but not for the generic case (because for that it lacks reflexivity and, oh :sweat_smile:, now I get your point: transitivity).

Not sure what you mean. I merely try to draw conclusions from this failing:

let _: &i32 = 0i32.as_ref(); // Won't work

If that fails (which is a pretty basic operation), then I feel like I can't expect this to work either:

let _: &i32 = Cow::<i32>::Owned(0i32).as_ref();

But the first fails, and the second works. I guess I tried to find a pattern / solution here by claiming that going from a generic "maybe reference-like type" (aka impl GenericCow<T>) to a reference type (&T) is something semantically different. But maybe it's not much different than going from PathBuf to &Path? Not sure. I think if was not semantically different, then AsRef would be messed up completely (which is basically your point, and which I stated here).

Yeah, of course it would break that pattern. I was talking about an "ideal world" here. Remember, in that case we'd have &P where P: ?Sized + AsRef<Path> and where AsRef is reflexive at least.

Not sure what you meant, but this is something I just tried based on your Playground:

// these "unsightly" ones:
fn foo<P: ?Sized + MyAsRef<i32>>(_: &P) {}
fn bar<P: ?Sized + MyAsRef<str>>(_: &P) {}
fn baz<P: ?Sized + MyAsRef<OsStr>>(_: &P) {}

fn main() {
    foo(&0i32);
    foo(&MyCow::Borrowed(&0i32));
    bar("Hello");
    bar(&"Hello".to_owned());
    bar(&MyCow::Borrowed("Hello"));
    bar(&MyCow::Owned("Hello".to_string()));
    // We run into a problem due to lack of transitivity here:
    //baz(&MyCow::Borrowed(Path::new("Hello")));
    let my_cow = MyCow::Borrowed(Path::new("Hello"));
    let path: &Path = my_cow.my_as_ref();
    baz(path);
}

(Playground)

:face_with_spiral_eyes:

Not sure if any of this would work out. And now I'm not even sure if there is a proper solution at all, i.e. if #98905 really is a bug. Maybe this rather leads to an impossibility theorem!? No idea.

If that was the case: Sorry for the noise.


P.S.: I wrote an update to my bug report.


  1. And made learning the language a tiny bit more difficult. ↩︎

  2. Not because there is an explicit guarantee that it compiles but because people apply their knowledge regarding AsRef and Borrow. ↩︎

  3. Which is what I also suggested in a comment to my bug report: "If the cause of the issue cannot be fixed, it should be at least documented as an error in Cow's documentation." Maybe I phrased that a bit hard. ↩︎

  4. Maybe someday we'll have enough specialization and inference improvements to get some of these ideas implemented, but we're probably talking 5-10 years or more in the future, if ever. ↩︎

1 Like