I am interested in using Rust’s new associated types feature to reduce the amount of boilerplate in some old code of mine that has generic traits and implementations with very scary parameter lists. Consider the following example, which does not use associated types:
// The result of trying to read an element from an iterator
pub enum GetResult<Iterator, Element, Leftovers> {
GetCont(Element, Iterator), // Element successfully read
GetDone(Leftovers) // End of iterator
}
// We encode an ML-like signature for iterators as a trait
// Every type member of the signature is a generic parameter
/* module type INPUT_ITERATOR = sig
* type iterator
* type element
* type leftovers
* val get : iterator -> (iterator, element, leftovers) get_result
* end
*/
pub trait InputIterator<Iterator, Element, Leftovers> {
fn get(self, Iterator) -> GetResult<Iterator, Element, Leftovers>;
}
It is natural to want to be able to zip iterators:
pub struct Zip<L, R> { left: L, right: R }
pub enum ZipInputLeftovers<LI, LE, LL, RI, RE, RL> {
MoreL(LE, LI, RL), // Left iterator stopped first
MoreR(LL, RE, RI), // Right iterator stopped first
Neither(LL, RL) // Both iterators stopped at the same time
}
impl< LI, LE, LL, L: InputIterator<LI, LE, LL>,
RI, RE, RL, R: InputIterator<RI, RE, RL> >
Iterator< (LI, RI), (LE, RE),
ZipInputLeftovers<LI, LE, LL, RI, RE, RL> >
for Zip<L, R> {
fn get(self, (li, ri): (LI, RI)) -> GetResult< (LI, RI), (LE, RE),
ZipLeftovers<LI, LE, LL, RI, RE, RL> > {
match (self.left.get(li), self.right.get(ri)) {
(GetCont(le, li), GetCont(re, ri)) => GetCont( (le, re), (li, ri) ),
(GetCont(le, li), GetDone(rl)) => GetDone( MoreL(le, li, rl) ),
(GetDone(ll), GetCont(re, ri)) => GetDone( MoreR(ll, re, ri) ),
(GetDone(ll), GetDone(rl)) => GetDone( Neither(ll, rl) )
}
}
}
With associated types, as documented in the RFC, some of the boilerplate can be reduced:
pub trait InputIterator {
type Iterator;
type Element;
type Leftovers;
fn get(self, Iterator) -> GetResult<Iterator, Element, Leftovers>;
}
impl<L: InputIterator, R: InputIterator> InputIterator for Zip<L, R> {
type Iterator = (L::Iterator, R::Iterator);
type Element = (L::Element, R::Element);
type Leftovers = ZipInputLeftovers<
L::Iterator, L::Element, L::Leftovers,
R::Iterator, R::Element, R::Leftovers >;
fn get(self, (li, ri): Iterator) -> Get<Iterator, Element, Leftovers> {
// ...
}
}
However, some of the annoyances remain:
-
We still need generic types with long type parameter lists.
GetResulthas 3 type parameters.ZipInputLeftovershas 6 type parameters. Some of the scariest iterators in my library have associated types with more than 10 (!) type parameters. -
It is possible to instantiate
GetResultwith nonsensical types, for instanceGetCont("hello", 1)has typeGetResult<int, &'static str, T>. How does it make sense forintto be an iterator type? Things get even worse withZipInputLeftoversand the other bigger generic types.
I propose that these issues can be avoided by allowing traits and implementations to contain not just associated type synonyms, but also new type definitions (that is, structs and enums):
// The ML analogue is allowing translucent signatures....
/* module type INPUT_ITERATOR = sig
* type iterator
* type element
* type leftovers
* type result = CONT of element * iterator | DONE of leftovers
* val get : iterator -> result
* end
*/
pub trait InputIterator {
type Iterator;
type Element;
type Leftovers;
enum Result {
Cont(Element, Iterator),
Done(Leftovers)
}
fn get(self, Iterator) -> Result;
}
impl<L: InputIterator, R: InputIterator> InputIterator for Zip<L, R> {
type Iterator = (L::Iterator, R::Iterator);
type Element = (L::Element, R::Element);
enum Leftovers {
MoreL(L::Element, L::Iterator, R::Leftovers),
MoreR(L::Leftovers, R::Element, R::Iterator),
Neither(L::Leftovers, R::Leftovers)
}
fn get(self, (li, ri): Iterator) -> Result {
match (self.left.get(li), self.right.get(ri)) {
(L::Cont(le, li), R::Cont(re, ri)) => Cont( (le, re), (li, ri) ),
(L::Cont(le, li), R::Done(rl)) => Done( MoreL(le, li, rl) ),
(L::Done(ll), R::Cont(re, ri)) => Done( MoreR(ll, re, ri) ),
(L::Done(ll), R::Done(rl)) => Done( Neither(ll, rl) )
}
}
}
To my taste, this code is more pleasant for the following reasons:
-
Gone are the long generic parameter lists. Because
Zip<L,R>::Leftoversis defined in a context in whichLandRare both known to implementInputIterator, we have direct access to all six types{L, R}::{Iterator, Element, Leftovers}. -
It is no longer possible to construct
Zip<L, R>::Leftoversvalues from things that are not legitimately iterators, elements and leftovers. The type system now checks more of the program’s correctness for us.
An important observation is that this design breaks if we allow the type member InputIterator::Result to be overriden in implementations. Notice that the implementation of InputIterator for Zip<L, R> pattern-matches on the constructors of L::Result and R::Result.
Summarizing, my proposal consists of two things:
- Adding associated new types (structs and enums).
- Preventing associated new types being overriden (unlike associated type synonyms).
Do you guys think this proposal could make it as an extension to the associated types RFC?