Dependency update caused `AsRef` to become ambiguous

I had some previously working code that called as_ref on a Cow<str> to get a &str. The code broke after updating my dependencies (with cargo update), failing to compile with an error about missing type annotations:

error[E0283]: type annotations needed
   --> src/ui/pages/phoneme_tables/custom/move_phoneme_popover.rs:106:54
    |
106 |                 .label(phoneme.user_symbol(extras.get(&phoneme)).as_ref())
    |                                                                  ^^^^^^
    |
    = note: multiple `impl`s satisfying `Cow<'_, str>: AsRef<_>` found in the following crates: `alloc`, `typed_path`:
            - impl<T> AsRef<T> for Cow<'_, T>
              where T: ToOwned, T: ?Sized;
            - impl<T> AsRef<typed_path::common::utf8::path::Utf8Path<T>> for Cow<'_, str>
              where T: typed_path::common::utf8::Utf8Encoding;
help: try using a fully qualified path to specify the expected types
    |
106 -                 .label(phoneme.user_symbol(extras.get(&phoneme)).as_ref())
106 +                 .label(<Cow<'_, str> as AsRef<T>>::as_ref(&phoneme.user_symbol(extras.get(&phoneme))))

I spent some time debugging the issue. It seems to be caused by an update to the zip dependency, which adds a dependency on typed-path.

I've created a small reproduction of the issue:

use std::borrow::Cow;
// use typed_path::Path;

fn main() {
    let x = Cow::Borrowed("hello");
    let y = x.as_ref();
    println!("{y}");
}

This code compiles fine as-is, but fails to compile if the use typed_path::Path line is uncommented (after running cargo add typed-path, of course).

However, my code doesn't import typed_path::Path anywhere, and the file that contains the compiler error doesn't import zip.


This was a surprising error to encounter, and I'm not really sure how best to avoid it, either as a downstream user of zip, or as a hypothetical library author adding such a dependency.

  • How should I, as a dependant on the zip crate, avoid this? Is my use of Cow::as_ref considered "brittle"? Should I avoid it writing code like function(foo.as_ref()), preferring something like let bar: &str = foo.as_ref(); function(bar)?
  • How could the zip authors (or the authors of any other library) avoid causing this in future? Should they? Is there a way that they or others would be able to tell that adding the typed-path dependency would constitute a break in downstream code using Cow<str>::as_ref?
  • Would this be considered a semver-breaking update to zip?
  • Adding a non-exported dependency doesn't usually cause semver breaks, as far as I know. Have the typed-path developers "done something wrong"?

Thanks for getting through all these complex questions :sweat_smile:

1 Like

If you get type inference breakage for as_ref(), that likely means you are using it inappropriately. It's only supposed to be used when the target type is known. Unfortunately, it's very easy to use when the target type isn't known and rely on inference instead, which will happily select the appropriate trait impl when there's only one of them. But as soon as a second one is added (which can happen by simply updating or adding a dependency), as you discovered, the trait impl to select is no longer unambiguous. It feels a lot like "spooky action at a distance," but it should always be resolvable by either adding type annotations or using a better method to get a ref.

For example, to get a &[T] from a Vec<T>, you should use x.as_slice() or &*x, but not x.as_ref() unless you're specifically using the as_ref() in a generic context:

fn take_slice_from_anything(slice: impl AsRef<[T]>) {
    let slice = slice.as_ref(); // this is okay! because the target type is fixed to `&[T]`
}

See improve documentation for AsRef/AsMut · Issue #62586 · rust-lang/rust · GitHub for a suggestion to improve the docs on AsRef.

But no, this is generally not considered a semver breaking change.

1 Like

I think the issue reduces to the following: Rust Playground

mod foo {
    use std::borrow::Cow;

    fn bar() {
        let x = Cow::Borrowed("hello");
        let y = x.as_ref();
        println!("{y}");
    }
}

use std::borrow::Cow;

struct Foo;

impl AsRef<Foo> for Cow<'_, str> {
    fn as_ref(&self) -> &Foo {
        &Foo
    }
}

Usually, adding a new trait implementation is not considered a major breaking change that would warrant a major bump to the version number. I guess it must be sufficiently rare to hit issues passing an argument to a generic function like that. For whatever it’s worth, I prefer using &* to apply a deref instead of using .as_ref(), and Deref isn’t vulnerable to your problem.

2 Likes

Thanks!

Unfortunately, there doesn't seem to be a more explicit method for Cow::<str>, although I think the currently unstable str::as_str should suffice when it's added. Adding a &str type annotation does work.

This also works!