Pre-RFC: Associated type inference

Summary

Remove the need for explicit associated type definitions when implementing a trait where it can be inferred from a local trait method.

Motivation

When implementing a trait with generics or associated types, you often have to repeat types multiple times. For example:

impl Iterator for Foo {
    type Item = usize;
    fn next(&mut self) -> Option<usize> { ... }
}

Or a more complex one:

impl<S, R> Service<R> for Timeout<S>
where
    S: Service<R>
{
    type Response = S::Response;
    type Error = E;
    type Future = ResponseFuture<S::Future>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { ... }
    fn call(&mut self, request: R) -> Self::Future { ... }
}

Having to write out associated type definitions makes implementing a trait feel much heavier than writing an inherent method. This proposal introduces an inference source for these types, local trait methods.

impl Iterator for Foo {
    fn next(&mut self) -> Option<usize> { ... } // `Item = usize` is inferred
}

Guide-level explanation

An associated type can only be inferred if a concrete type is used in it's place in a trait method. This means that Iterator::Item can be inferred from the return type of next:

impl Iterator for Foo {
    fn next(&mut self) -> Option<usize> { ... } // `Item = usize`
}

However, IntoIterator::Item will not be:

impl IntoIterator for Foo {
    type Item =  usize; // Still required
    fn into_iter(self) -> FooIntoIter { ... } // `Iter = FooIntoIter`
}

Although it could be inferred from <Self::Iter as Iterator>::Item, this would significantly affect reasoning about the code as you would have to jump to a second trait implementation to find the type.

Drawbacks

Associated type inference removes some context about the trait from implementations. You will not be able to look at a FromStr implementation and see directly what the Error type is. Instead, you will have to look at the return type of the from_str method. A lot of this could be mitigated by having rustdoc retain full associated type definitions.

Alternatives

Type Elision

An alternative approach to removing the need for the type definition completely is using the _ placeholder:

impl Iterator for Foo {
    type Item = _;
    fn next(&mut self) -> Option<usize> { ... }
}

However, the benefit provided this is much less. You still need the type Item = _ line, yet it doesn't really provide any meaningful information.

Another approach might be to elide the function's parameter/return type:

impl Iterator for Foo {
    type Item = usize;
    fn next(&mut self) -> Option<_> { ... }
}

But this again doesn't help very much, you can already use the Self::Item alias instead of writing the type again, and you still need the extra line, type Item = T.

Inferring Generics

Generic types cannot be inferred in this proposal:

impl From<_> for Foo { // not allowed
   fn from(x: usize) -> Foo { ... }
}

The main reason for this is that inferring generics provides little benefit, saving a few characters versus saving an entire line with associated types. Generics also being a direct part of the trait means that scanning code for say, a From<X> implementation provides immediate context about a type. From<_> would mean an extra step to look at the parameter of the from method. With associated types this extra step is already there. Now instead of searching for impl Iterator and looking at type Item, you search for impl Iterator and look at fn next.

Generic inference is left as a potential future extension in an effort to keep this RFC minimal.

Prior art

I believe Haskell performs similar type inference?

5 Likes

Alternatives should mention using the associated type directly in the implementation to avoid repetition:

impl Iterator for Foo {
    type Item = usize;
    fn next(&mut self) -> Option<Self::Item> { ... }
}

This is my preferred way to write it currently, especially because it's the form you get when you copy-paste the trait definition from the docs and then fill out the bodies.

16 Likes

Thanks, I'll add that. The point of this proposal is to eliminate the extra type definition completely, which gets implementing a trait much closer to writing an inherent method.

Oh, this is neat. Writing the extra associated-type line is definitely a minor annoyance for me, and this seems like a logical way to avoid it.

That said, at first glance, I don't feel like this is worth changing the language for, especially since there may be a reduction in code readability. The status quo isn't a serious problem, so I'm inclined to think that the hidden costs that come with adding any language feature outweigh the benefit.

3 Likes

I wonder if this could allow for a reduced version of type_alias_impl_trait but only in traits.

I'm not convinced by the difference here. I think if it's important to be blatantly obvious in the source code -- as opposed to in the rustdoc, where it will be shown either way -- then we shouldn't do the feature at all. But if that's not particularly important, as implied by having the feature at all, then I don't see why it's needed for Item to be there.

I could probably be persuaded that there's a meaningful distinction, though.

I think the big missing alternative here is structured suggestions. For example, fn foo(x: i32) -> _ { x } doesn't work, because we want the return type specified, but there's a wonderful structured suggestion to replace the _ with exactly the right type.

So one stepping stone would be to make it so that it's not just the generic

help: implement the missing item: `type Output = Type;`

but runs the inference and gives the actual type Output = i32; or whatever that will work with the rest of the impl. (Just like is done for -> _.)

And that's "just" a diagnostic change, so doesn't require an RFC or other heavy process like that.

4 Likes

No. Instead, Haskell doesn't have type signatures on the methods of a trait implementation. Like, at all. You even need to activate an additional language extension to allow them at all, even optionally. Signatures of trait methods are basically completely redundant anyways, so it makes sense. However, associated types are always required to be specified.1 This is most similar to your alternative

but you wouldn't need to specify the Option either, or the &mut for self.

1Actually, you can leave them out if you want, but the effect is drastically different. The type isn't inferred but just left to be a kind-of abstract unknown type; which is something Haskell can do because it doesn't have monomorphization and all types are boxed, so they all have the same representation at runtime and you don't actually need to know the concrete type for code generation.

2 Likes

I think this one is a very different feature. Because it goes down the road of allowing this:

impl Add<SomeReallyComplicatedType> for Foo {
    type Output = Foo;
    fn add(self, other: _) -> Foo { ... }
}

where because it's the impl of a trait, the type there is fully specified anyway, so there's really no need to specify it -- having the compiler just fill it in with the only allowed value is substantially simpler than the inference needed to populate an associated type or TAIT.

(Said without taking a position either way on whether allowing such a thing would be good.)


This also makes me think of another potential idea in the same area as this.

Given a future in which we have trait aliases stabilized, imagine that you have trait ByteIter = Iterator<Item = u8>;. Then would could consider allowing

impl ByteIter for Foo {
    fn next(&mut self) -> Option<Self::Item> { ... }
}

without needing the associated type because it's "defined" by the equality constraint in the trait alias.

(Though right now you can't impl a trait alias at all, so there's a bunch more design work hiding in there.)

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.