The Hashtable Problem: A Litmus Test for External Impl Proposals
jrose:
> Because it was not obvious to me the first time I encountered this problem, the requirement of `K: Hash` is _not_ on the HashMap type in Rust; it's on the impl block containing operations that hash.
After thinking about this for ages, I've realised that the problem isn't really a problem with respect to types like `HashMap` for which the implemented trait is not `unsafe`. Not including `K: Hash` on the `HashMap` type itself means that it's impossible for external-impl proposals to ensure that `HashMap` is coherent in the sense of always hashing the same keys to the same values (because a different `Hash` impl could be used for each call); but even in current Rust, `HashMap` has numerous ways in which a programmer could cause incoherent hashing in a way that won't be caught by the compiler:
* (edit: I corrected this in response to pitaj's correction) `Hash` is a safe trait, so nothing forces `K: Hash` and ~~`&K: Hash`~~ `Q: Hash` (where `Q: Borrow<K>` is used to do hash lookups) to have any relationship to each other, but it is possible to add keys to a hash as `K` and then retrieve them using a `Q`;
* `Hash` is a safe trait, so nothing is forcing it to always hash the same key to the same hash (it could, e.g., return a random number)
* Even if `Hash` is deterministic, it is possible to use interior mutability to modify the keys stored in a hash table (while they are being stored) in order to change the value they would hash to.
`HashMap`'s documentation contains plenty of warnings against doing this sort of thing, but it is just warnings in the documentation (and in particular its memory safety is not allowed to depend on the programmer doing this). As such, a change to Rust to allow external impls wouldn't actually break `HashMap` in any ways that it isn't already broken: it would allow people to try to retrieve keys using a different hashing function from the one used to add them (which of course doesn't work), but there are plenty of ways to do that even without external impls.
In general, it seems impossible for incoherent implementations of a safe trait to break existing code, as long as all the trait's associated items are methods (i.e. it has no associated types (including RPITIT as a special case of associated types) or associated constants). That's because code like this, which is the case that breaks (stealing the syntax from the OP):
let g: Generic<T> = Generic::new();
(&g as &Generic<T + Trait use Impl1>).method();
(&g as &Generic<T + Trait use Impl2>).method();
could be replicated without external impls by doing something like this:
thread_local! { static USE_IMPL_2: Cell<bool> = const { Cell::new(false); }; }
impl Trait for T {
// for each method in the trait, you write a small wrapper like this
fn f(&self) {
if USE_IMPL_2.get() { self.f2(); } else { self.f1(); }
}
}
let g: Generic<T> = Generic::new();
USE_IMPL_2.set(false);
g.method();
USE_IMPL_2.set(true);
g.method();
This specifically only works for methods, because associated types or constants can't depend on the value of a global variable. (It also doesn't work for `unsafe` traits, because they may have safety requirements that prohibit this sort of implementation.) It strikes me that this is very similar for the rules of `dyn` compatibility (the only exception is that `dyn` compatibility requires the ability to make a vtable, whereas this doesn't), which is probably not entirely a coincidence.
All this is making me think that external impls should probably be designed so that putting a bound on the type itself (as opposed to the `impl` block) is the correct way to say "this trait must always be implemented the same way whenever a method of this type is called": for safe, `dyn`-compatible traits, this seems to be entirely backwards compatible with current Rust, and it also fits my expectation of how trait bounds should work (and is consistent with how type generics are specified: if the type parameter needs to always be the same, it's placed on the type itself, if it can differ between calls to a method it's placed on the method).
Further evidence that this is the correct approach is that there's already a place where existing Rust has what in effect is an external implementation, and it works like this already. I realised recently that a lifetime `'a` is equivalent to an external impl of `impl<T> Deref for &'a T` (and likewise for `Deref` and `DerefMut` on `&'a mut T`). (That is, you can view `&'a T` as a type that implements `Deref` conditionally on being inside the lifetime `'a`, and thus the lifetime itself can be viewed as an external impl of `Deref` (providing one possible view of what a lifetime actually _is_ , theoretically).) This means that any problem with externally implemented traits should also have an equivalent problem that shows up with lifetimes.
In the case of lifetimes, Rust prevents the equivalent of the hashtable problem arising by requiring any lifetime generics that exist within the types of `struct`/`enum` fields to also be lifetime generics of the type itself (thus forcing them to be the same every time). It seems that the fact that the generics exist on the type itself is important for soundness: there is at least one case in current Rust where a type involves types with a lifetime but they aren't generics on the type itself (closures, which can have a non-generic type even if they capture a value whose type has a lifetime), which creates a soundness hole in the type system. This is part of what makes me expect that the same solution would be used for external implementations other than lifetimes.