Motivation
Extension traits have an ergonomics problem as downstream must have explicit use statements to use their inherent methods. This is especially frustrating for fields in data structures. For instance, I have an extension trait for Vec<u8> and String which adds accounting to their allocations. That is, allocating methods have an alternative with additional argument that accumulates the bytes in-use. (None of this is safety critical).
let mut vec = vec![];
vec.extend(iterator);
// becomes
match vec.acc_extend(&mut account, iterator) {
Ok(()) => { /* all items consumed */ },
Err(actual) => { /* not enough capacity in account */ },
}
For several reasons this does not make a good wrapper type. Firstly, not all methods on Vec need access to an account and providing all of them would contribute to complexity. Secondly, the account is suppose to be shared but using Arc/Rc to have handles to the same account in multiple containers imposes additional costs and design complexity. Thirdly, for purposes of API compatibility the Vec is just too much of a vocabulary type. Fourth, maintaining a list of forwarded methods is hard for unstable methods; and the account need only be managed at the user boundary.
A similar issue appears in the evolution of primitives. Expanding the set of methods on NonZero* and f32 with more numeric operations in the standard library requires care. It would be nice to prototype or even keep some of the more elaborate methods (e.g. methods from statistics, deterministic soft-float, approximate&fast trigonometry, fast-inverse square root) in a crate. Here we also want to keep the actual data type f32 so it composes with type constructor, nalgebra, gemm implementations, etc. It's common enough to declare just data types that are manipulated elsewhere. There's some mental overhead in importing both the container as well as potentially multiple extension traits for floating point types in all use sites separately.
Idea
Let a type alias declare trait bounds in addition to their base type which are considered in-scope when accessing a place of the designated type.
// In utils.rs
trait AccountVec {
type Item;
fn acc_reserve(&mut self, _: &mut Account, sz: usize) -> Result<(), usize>;
}
// as an alias definition
type AccountedVec<T> = Vec<T> + use<crate::utils::AccountVec>;
// in module user.rs
fn using_this(acc: &mut Account) {
let mut container: utils::AccountedVec<_> = vec![];
// _no_ use `crate::utils::AccountVec`
let _ = container.acc_reserve(acc, 100);
}
Also importantly this should work for fields in structs downstream in different crates:
use accounted::{Account, AccountedVec, AccountedHashMap, AccountedOsString};
enum Header { /* not relevant */ }
pub struct DecodingBuffer {
raw: AccountedVec<u8>,
headers: AccountedVec<Header>,
by_name: AccountedHashMap<HeaderName, usize>,
filename: AccountedOsString,
account: Account,
}
This would behave much more closely as the type having the extra methods than using extensions through a separately named trait. I think it is significantly discoverable when the type alias is documented. There will be roughly one trait per type (family), but you only discover that type family in the documentation by looking at the implementations for the trait—which is not indexed or found by the search feature especially if the trait is not named by its base type.
More example
Slices can natively only be indexed by usize. But strongly-typed indexes are a nice tool to ensure that the right numbers are used in the same manner, i.e. a separate type for indices that went through a validation of sorts. SliceIndex is unstable so we do that via an extension trait to slices instead. In the interface boundary however it would be nice to denote that relationship:
mod internal {
trait IndexWith<U>: ?Sized { /* stable copy of SliceIndex */ }
impl<T> IndexWith<WrapperA> for [T] { … }
type IndexedItem = [ItemForA] + use<IndexWith>;
}
fn this_will_index(slice: &IndexedItem, name: WrapperA) {
slice.get_with(name);
}
Alternatives
Consider the alternative to the above example:
/* Imports disjoint but semantically only useful together */
use std::ffi::OsString;
use std::collections::HashMap;
use accounted::{Account, AccountVec as _, AccountOsString as _, AccountHashMap as _};
enum Header { /* not relevant */ }
pub struct DecodingBuffer {
raw: Vec<u8>,
headers: Vec<Header>,
by_name: HashMap<HeaderName, usize>,
filename: OsString,
account: Account,
}
Another problem with the above approach is that, for a generic trait, it brings the whole trait into scope. However, the feature is intended to improve visibility and the real use might want to consider a single specific impl only. In the index type example above this is obvious:
// Suppose:
// impl IndexWith<WrapperA> for [T]
// impl IndexWith<WrapperB> for [T]
fn this_will_index(slice: &IndexedItem) {
let wrapper: WrapperB = todo!();
// Oops, we meant to have allow a `WrapperA` only.
slice.get_with(wrapper);
}
Alternatively what if could we bring a specific impl into scope for the type, instead of a whole trait? This would come with additional complexity such as error messages and semantics that are not found anywhere else.
type SliceIndexByA<T> = [T] + IndexWith<WrapperA>;
fn this_will_index(slice: &IndexedItem) {
let wrapper: WrapperB = todo!();
slice.get_with(wrapper);
// ^^^^ Error: mismatched types; expected WrapperA found WrapperB
//
// help: trait `IndexWith` which provides `get_with` is implemented but only the
// impl `IndexWith<WrapperA>` is in scope; perhaps you want to import it
}
Open questions
I'm not sure if this is compatible with the compiler's implementation of type alias. Also does it affect validity, we must not be allowed to create incoherent bounds with this…
How does this interact with type deduction?