tamuhey
tamuhey

Reputation: 3535

Is it possible to convert `Borrow<T>` to `AsRef<T>` or vice versa?

I have a variable tokens: &[AsRef<str>] and I want to concatenate it into single string:

// tokens: &[AsRef<str>]
let text = tokens.join("") // Error
let text = tokens.iter().map(|x|x.as_ref()).collect::<Vec<_>>().join("") // Ok, but...

The second is awkward and inefficient because it reallocates items to a new Vec.

According to the source code, join can be applied to tokens if its type is &[Borrow<str>]:

// if tokens: &[Borrow<str>]
let text = tokens.join("") // OK

// so I want to convert &[AsRef<str>] to &[Borrow<str>]
let text = convert_to_borrow(tokens).join("")

How should I do that? Why is Join implemented for types that implement Borrow but not AsRef?

Upvotes: 1

Views: 423

Answers (2)

Peter Hall
Peter Hall

Reputation: 58785

Trentcl's answer gives you a solution to your actual problem by relying only on the AsRef<str> implementation. What follows is more of an answer to the more general question in your title.

Certain traits carry with them invariants which implementations must enforce. In particular, if implementations of Borrow<T> also implement Eq, Hash, and Ord then the implementations of those traits for T must behave identically. This requirement is a way of saying that the borrowed value is "the same" as the original value, but just viewed in a different way. For example the String: Borrow<str> implementation must return the entire string slice; it would be incorrect to return a subslice.

AsRef does not have this restriction. An implementation of AsRef<T> can implement traits like Hash and Eq in a completely different way from T. If you need to return a reference to just a part of a struct, then AsRef can do it, while Borrow cannot.

All this means that you cannot derive a valid Borrow<T> implementation from an arbitrary AsRef<T> implementation: the AsRef implementation may not enforce the invariants that Borrow requires.

However, the other way around does work. You can create an implementation of AsRef<T>, given an arbitrary Borrow<T>:

use std::borrow::Borrow;
use std::convert::AsRef;
use std::marker::PhantomData;

pub struct BorrowAsRef<'a, T: ?Sized, U: ?Sized>(&'a T, PhantomData<U>);

impl<'a, T, U> AsRef<U> for BorrowAsRef<'a, T, U>
where
    T: Borrow<U> + ?Sized,
    U: ?Sized,
{
    fn as_ref(&self) -> &U {
        self.0.borrow()
    }
}

pub trait ToAsRef<T: ?Sized, U: ?Sized> {
    fn to_as_ref(&self) -> BorrowAsRef<'_, T, U>;
}

impl<T, U> ToAsRef<T, U> for T
where
    T: ?Sized + Borrow<U>,
    U: ?Sized,
{
    fn to_as_ref(&self) -> BorrowAsRef<'_, T, U> {
        BorrowAsRef(self, PhantomData)
    }
}
fn borrowed(v: &impl Borrow<str>) {
    needs_as_ref(&v.to_as_ref())
}

fn needs_as_ref(v: &impl AsRef<str>) {
    println!("as_ref: {:?}", v.as_ref())
}

Why is Join implemented for types that implement Borrow but not AsRef?

This is a blanket implementation, for all types that implement Borrow<str>, which means it can't also be implemented for types that implement AsRef<str>. Even with the unstable feature min_specialization enabled, it wouldn't work because having an implementation of AsRef is not more "specific" than having an implementation of Borrow. So they had to pick one or the other.

It could be argued that AsRef would have been a better choice because it covers more types. But unfortunately I don't think this can be changed now because it would be a breaking change.

Upvotes: 4

trent
trent

Reputation: 28005

It may be slightly slower, but you can collect an iterator of &strs directly into a String.

let text: String = tokens.iter().map(|s| s.as_ref()).collect();

This is possible because String implements FromIterator<&'_ str>. This method grows the String by repeatedly calling push_str, which may mean it has to be reallocated a number of times, but it does not create an intermediate Vec<&str>. Depending on the size of the slices and strings being used this may be slower (although in some cases it could also be slightly faster). If the difference would be significant to you, you should benchmark both versions.

There is no way to treat a slice of T: AsRef<str> as if it were a slice of T: Borrow<str> because not everything that implements AsRef implements Borrow, so in generic code the compiler can't know what Borrow implementation to apply.

Upvotes: 4

Related Questions