Safe coercion of &F<Box<T>> to &F<&T>

Rust guarantees that Box<T> is representationally equivalent to &T.

Given a &Box<T>, there isn't much you can do except to extract &T from it. So would it be fair to say that a function that operates on a &Box<T> is representationally equivalent to a function that operates on &&T instead? Can this be extended to aggregate data structures such as &(Box<Box<T>>, &mut U) vs &(&&T, &U)? What about custom structs and enums?

This came to mind when thinking about how ownership considerations of composite data structures can limit API design in Rust. Currently, either the library author has to carefully plan out the ownership of all data structures, which limits the user's flexibility, or they have to expose complex, generic-laden APIs to permit ownership flexibility.

For example, a lot of the time when a complicated struct is declared, e.g.

struct MyConfig {
    name: String,
    items: Vec<String>,
    ...
}

impl MyConfig {
    fn my_api(&self) { ... }
}

the library only wants to read the data, so it ends up not really caring whether the client provides name: String or name: &str, or whether Vec<String> or Vec<&str> or &[&str] is given. Yet, the naive approach would limit what the user can provide. If the library author wanted more flexibility, they would have to write something terrible like:

struct MyConfig<N, I, ...> {
  name: N,
  items: I,
  ...
}

impl<N: Deref<Target=str>, I: Deref<Target=[impl Deref<Target=str>]>, ...> MyConfig<N, I> {
    fn my_api(&self) { ... }
}

In a hypothetical Rust variant where &str is representationally compatible with and coercible [1] from String, and similarly for &[T] from Vec<T>, then it wouldn't be necessary to use generics for this at all, which introduces both mental complexity as well as needless code bloat. A single function ought to be able to cover all possible ownership configurations.

[1]: Sadly this doesn't work in real Rust because String has an extra field compared to &str.

1 Like

IRL that is very straightforward in like 99% of cases: composite data structures should own their components, end of story. Yes, you can use generics to allow references in such places, but you don't have to; there are other approaches, the most obvious is Cow.

In any case, I wouldn't try to avoid generics deeming them "terrible". They are more abstract than concrete types, but they are more powerful and exist precisely to solve a large spectrum of similar problems in one go, rather than having to introduce all sorts of narrow-scoped one-off conversions.

At the end of the day, if there happens to be contention between library author convenience and library user convenience, the latter should usually win, as a library is written only once but used more than once, and hopefully has more users than authors. (Not to mention teachability – the idea of a library is to abstract away some complexity so that other people don't have to understand how it works.)

Something that I did see people struggle with while writing real code is that some traits designed for such abstraction require references, and as such, they can't be used with other, conceptually non-owning types, e.g. wrappers around references. An example is Borrow, which you can't implement by returning your own wrapper type; you have to return a reference. I think the introduction of new, less restrictive conversion traits would be way more useful, and it wouldn't need a language change. It could also be prototyped as a crate before putting it into std and committing to supporting it forever.

2 Likes

In general no, as one could implement traits differently for Box<T> and &T, and if that trait has an associated type then Foo<Box<T>> and Foo<&T> can be arbitrarily different.

3 Likes

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