So on the topic of whether families are “doing work the compiler should be doing for me”: I am certainly sympathetic, but also not. I am not quite sure how to order my thoughts yet on this topic, and I hope to get at it in more detail in a future post, but I’ll take a stab here. Anyway, here are some thoughts from reading the latest comments
On the question of HKT vs ATC
First, I second @nrc and @withoutboats in saying that I really want to get a better handle on the use cases for HKT independent of associated-type constructors. As @withoutboats and @Manishearth said, it seems like ATC is basically a no brainer: it’s a natural syntax, it fills a gap in the language.
In fact, when you consider that every method has a unique (unwritable, atm) type, we already have ATC. That is, if I have something like this:
trait Foo {
fn bar<'a, T>(...);
}
The type of Foo::bar includes the 'a and T that you supply to a particular instantiation (at least, some of the time – sometimes the type of bar is higher-ranked; c.f. the distinction between early- and late-bound regions; I am still puzzling a bit over the connection between higher-ranked and higher-kinded =)
Whether families are a poor man’s HKT or what
On the one hand, the “family pattern” is definitely emulating HKT to some extent. On the other hand, it’s also more expressive than conventional HKT: for example, it can accommodate bounds. As I noted in the post, basically no types in Rust actually satisfy a kind like (type -> type), since most of them require something more like type: Sized -> type.
Having to add things like I<_> into the language is already adding complexity – and it hasn’t even addressed the need for bounds! Presumably we’d wind up writing things like for<K: Hash> I<K> or something at some point. We won’t want to repeat these things everywhere, so we’ll need to invent ways to factor out these signatures into independent declarations – at which point we are basically reinventing families. So it’s not clear to me that HKT is really going to be a win syntactically.
How often do we really want “true HKT” anyway?
But there is a separate question. Let’s assume that that we either don’t add HKT (in which case we use families) or we invent some syntax for HKT that expresses bounds – how often will we find ourselves wanting to abstract over type constructors anyway? In particular, how often will we want to abstract over a type constructor where you don’t have a specific instance around? (I’ll just use families in my examples, since they have a concrete syntax.)
That is, how often do you want to write functions or types that are generic over Vec but which do not have an argument or return type like Vec<T> (Vec for some specific T)? (After all, if you do, you then don’t really want a family, you just want a way to go from one kind of collection to another instance in that same family, which is less modeling overhead.)
But there’s another question that I’m also wrestling with. Collections are actually a really bad case for HKT, because they vary so wildly in the bounds that they impose. e.g., if I want to write some code that works for any set, the conditions to make a valid set will be different for hashsets and btreesets. It’ll be hard to accommodate this even in families. Consider this very simple attempt:
trait SetFamily {
type Member<K>;
}
This is clearly wrong, since it imposes no conditions on K. So maybe we write:
trait SetFamily {
type Member<K: Hash + Ord>;
}
Now both HashSet and BTreeSet can implement it, but it’s actually too strong for both of them! It’s hard to see what’s the right balance here, unless we let you abstract over trait bounds (and I’m not ready to go there).
I’m not sure what the answer is here! One pattern I was toying with was something like this, which actually doesn’t use either ATC or HKT, just HRTB that apply to types:
/// If you have `Foo: Set<T>`, then the type `Foo` can act as a set of `U`.
trait Set<T> { /* methods for some paricular set */ }
/// If you have `Foo: SetApply<T, U>`, then not only is `Foo` a set of `U`,
/// but `<Foo as SetApply<T, U>::Member` is a set of `U` in the same "family".
trait SetConvert<T, U>: Set<T> {
type Member: Set<U>;
}
impl<T: Hash, U: Hash> SetConvert<U> for HashSet<T> { ... }
impl<T: Ord, U: Ord> SetConvert<U> for BTreeSet<T> { ... }
(Probably I would actually use an associated type for the T in these examples, but whatever.)
Anyway, the key idea here is that we have pulled the new element type for the set (U) out into the trait. This is useful because it means that we can let the functions that are consuming the set specify the limits they wish to impose on their keys:
/// If you give specific types, this is most flexible for your caller:
/// they can pick any set type that can accommodate those two types.
fn floatify<S>(s: S) -> S::Member
where S: SetConvert<i32, f32>
/// Or I can pick bounds. Here I say that you can use any kind of set,
/// as long as it can work with `Ord` types.
fn floatify<S>(s: S) -> S::Member
where S: for<U: Ord> SetConvert<i32, U>
/// Or I can pick bounds. Here I say that you can use any kind of set,
/// as long as it can work with hash + ord types.
fn floatify<S>(s: S) -> S::Member
where S: for<U: Hash+Ord> SetConvert<i32, U>
This seems to very much mirror the problems that HKT faces around collection types in general, no?