Generalizing borrows

A lot of core traits are built around some sort of field projection, where the implementing type contains some other type T and you can convert a &self to a &T or &mut self to a &mut T.

#![allow(unused)]
fn main() {
pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}

pub trait Index<Idx: ?Sized> {
    type Output: ?Sized;
    fn index(&self, index: Idx) -> &Self::Output;
}

pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}

pub trait Borrow<Borrowed: ?Sized> {
    fn borrow(&self) -> &Borrowed;
}

// `DerefMut`, `IndexMut`, `AsMut`, ...
}

There's generally no way to implement these traits if the type you want to return is not contained within Self (except for returning a reference to some static value or similar, which is rarely what you want).

However, sometimes you have a custom borrowing type which is not actually contained within your owning type:

#![allow(unused)]
fn main() {
// We wish we could implement `Borrow<DataRef<'?>>`, but we can't
pub struct Data {
    first: usize,
    others: Vec<usize>,
}

pub struct DataRef<'a> {
    first: usize,
    others: &'a [usize],
}

pub struct DataMut<'a> {
    first: usize,
    others: &'a mut Vec<usize>,
}
}

This can be problematic when interacting with libraries and data structures such as std::collections::HashSet, which rely on the Borrow trait to be able to look up entries without taking ownership.

One way around this problem is to use a different library or type which is more flexible. However, it's also possible to tackle the problem with a bit of indirection and type erasure.

Your types contain a borrower

Here we present a solution to the problem by Eric Michael Sumner, who has graciously blessed its inclusion in this guide. I've rewritten the original for the sake of presentation, and any errors are my own.

The main idea behind the approach is to utilize the following trait, which encapsulates the ability to borrow self in the form of your custom borrowed type:

#![allow(unused)]
fn main() {
pub struct Data { first: usize, others: Vec<usize> }
pub struct DataRef<'a> { first: usize, others: &'a [usize] }
pub trait Lend {
    fn lend(&self) -> DataRef<'_>;
}

impl Lend for Data {
    fn lend(&self) -> DataRef<'_> {
        DataRef {
            first: self.first,
            others: &self.others,
        }
    }
}

impl Lend for DataRef<'_> {
    fn lend(&self) -> DataRef<'_> {
        DataRef {
            first: self.first,
            others: self.others,
        }
    }
}

// impl Lend for DataMut<'_> ...
}

And the key insight is that any implementor can also coerce from &self to &dyn Lend. We can therefore implement traits like Borrow, because every implementor "contains" a dyn Lend -- themselves!

#![allow(unused)]
fn main() {
pub struct Data { first: usize, others: Vec<usize> }
pub struct DataRef<'a> { first: usize, others: &'a [usize] }
pub trait Lend { fn lend(&self) -> DataRef<'_>; }
impl Lend for Data {
   fn lend(&self) -> DataRef<'_> {
       DataRef {
           first: self.first,
           others: &self.others,
       }
   }
}
impl Lend for DataRef<'_> {
   fn lend(&self) -> DataRef<'_> {
       DataRef {
           first: self.first,
           others: self.others,
       }
   }
}
use std::borrow::Borrow;

impl<'a> Borrow<dyn Lend + 'a> for Data {
    fn borrow(&self) -> &(dyn Lend + 'a) { self }
}

impl<'a, 'b: 'a> Borrow<dyn Lend + 'a> for DataRef<'b> {
    fn borrow(&self) -> &(dyn Lend + 'a) { self }
}

// impl<'a, 'b: 'a> Borrow<dyn Lend + 'a> for DataMut<'b> ...
}

This gives us a common Borrow type for both our owning and custom borrowing data structures. To look up borrowed entries in a HashSet, for example, we can cast a &DataRef<'_> to a &dyn Lend and pass that to set.contains; the HashSet can hash the dyn Lend and then borrow the owned Data entries as dyn Lend as well, in order to do the necessary lookup comparisons.

That means we need to implement the requisite functionality such as PartialEq and Hash for dyn Lend. But this is a different use case than our general solution in the previous section. In that case we wanted PartialEq for our already-type-erased dyn Trait, so we could compare values across any arbitrary implementing types.

Here we don't care about arbitrary types, and we also have the ability to produce a concrete type that references our actual data. We can use that to implement the functionality; there's no need for downcasting or any of that in order to implement the requisite traits for dyn Lend. We don't really care that dyn Lend will implement PartialEq and Hash per se, as that is just a means to an end: giving HashSet and friends a way to compare our custom concrete borrowing types despite the Borrow trait bound.

First things first though, we need our concrete types to implement the requisite traits themselves. The main thing to be mindful of is that we maintain the invariants expected by Borrow. For this example, we're lucky enough that our borrowing type can just derive all of the requisite functionality:

#![allow(unused)]
fn main() {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct DataRef<'a> {
    first: usize,
    others: &'a [usize],
}

#[derive(Debug, Clone)]
pub struct Data {
    first: usize,
    others: Vec<usize>,
}

#[derive(Debug)]
pub struct DataMut<'a> {
    first: usize,
    others: &'a mut Vec<usize>,
}
}

However, we haven't derived the traits that are semantically important to Borrow for our other types. We technically could have in this case, because

  • our fields are in the same order as they are in the borrowed type
  • every field is present
  • every field has a Borrow relationship when comparing with the borrowed type's field
  • we understand how the derive works

But all those things might not be true for your use case, and even when they are, relying on them creates a very fragile arrangement. It's just too easy to accidentally break things by adding a field or even just rearranging the field order.

Instead, we implement the traits directly by deferring to the borrowed type:

#![allow(unused)]
fn main() {
struct Data; impl Data { fn lend(&self) {} }
// Exercise for the reader: `PartialEq` across all of our
// owned and borrowed types :-)
impl std::cmp::PartialEq for Data {
    fn eq(&self, other: &Self) -> bool {
        self.lend() == other.lend()
    }
}

impl std::cmp::Eq for Data {}

impl std::hash::Hash for Data {
    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
        self.lend().hash(hasher)
    }
}

// Similarly for `DataMut<'_>`
}

And in fact, this is exactly the approach we want to take for dyn Lend as well:

#![allow(unused)]
fn main() {
pub struct DataRef<'a> { first: usize, others: &'a [usize] }
pub trait Lend { fn lend(&self); }
impl std::cmp::PartialEq<dyn Lend + '_> for dyn Lend + '_ {
    fn eq(&self, other: &(dyn Lend + '_)) -> bool {
        self.lend() == other.lend()
    }
}

impl std::cmp::Eq for dyn Lend + '_ {}

impl std::hash::Hash for dyn Lend + '_ {
    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
        self.lend().hash(hasher)
    }
}
}

Whew, that was a lot of boilerplate. But we're finally at a place where we can store Data in a HashSet and look up entries when we only have a DataRef:

use std::collections::HashSet;
let set = [
    Data { first: 3, others: vec![5,7]},
].into_iter().collect::<HashSet<_>>();

assert!(set.contains::<dyn Lend>(&DataRef { first: 3, others: &[5,7]}))

// Alternative to turbofishing
let data_ref = DataRef { first: 3, others: &[5,7]};
assert!(set.contains(&data_ref as &dyn Lend));

Here's a playground with the complete example.

Another alternative to casting or turbofish is to add an as_lend(&self) -> &dyn Lend + '_ method, similar to many of the previous examples.