Niko, in your 3rd blog entry, you describe HKT versus ATCs as having their own tradeoffs and expressiveness potential. Specifically you describe the bounds requirements of hashsets and the non-genericity of bitsets. This reminds me of a similar problem thatâs already been solved before in Rust: having a hierarchy of traits to model increasing amounts of âpowerâ. The closure hierarchy of FnOnce, FnMut, and Fn is elegant and has proven to be very useful in Rust because people choose how much flexibility they need based on the problem at hand. For bitsets and Collections vs. HKTCollections, we find the same kind of choice. Let me proceed with some arbitrary definitions: the largest group of types we want to have methods for are things that are âCollectionsâ over some (single) type. These can return iterators, add to their collection, create new empty collections, etc. A subset of these collection types are âContainersâ, collections that are capable of holding many different kinds of types and are not bound to any particular one. These have the ability to map over them functor style, implement your floatify function in your second blog post, and most importantly do not require excessive type annotations to constrain the polymorphism/inference.
So if we think of HKTs as being a small improvement to ACTs, we can come up with this hierarchy:
// Collection trait
trait Collection<T> {
// create an empty collection of this type:
fn empty() -> Self;
// add `value` to this collection in some way:
fn add(&mut self, value: T);
// iterate over this collection:
fn iterate(&'a self) -> Self::Iter<'a>;
// the type of an iterator for this collection (e.g., `ListIter`)
type Iter<'a>: Iterator<Item=T>;
}
// "A collection that can contain many different types"
// I take some liberty here with the syntax
trait Container where Self<_>, for<T> Self<T> : Collection<T> {
...
}
impl Collection<usize> for BitSet { ... }
impl<T> Collection<T> for Vec<T> { ... }
impl Container for Vec { ... }
Etc.
Orthogonal consideration: you mentioned hashsets. Two people also mentioned constraint kinds / âassociated boundsâ as a solution. This seems like a pretty big leap in complexity but is certainly as intuitive as associated types are. In fact, because of Rustâs handy âdefault values for associated typesâ, this complexity is totally transparent to users until they need or want the power. Again, I take liberties with the syntax here:
trait Container<trait C = Id> where
Self<_>, for<T: C> Self<T> : Collection<T> {
...
}
impl<T: Hash> Collection<T> for HashSet<T> { ... }
impl Container<Hash> for HashSet { ... }
Where Id is a new built in trait auto-implemented for everything and means nothing (similar to the Id wrapper type as commonly used in Haskell). In essence: if you donât specify a trait, there is no trait requirement (I would expect this pattern a lot). That particular pattern is also backwards compatible.
I think this is very elegant!