Impl Trait in generic type arguments


#1

I was writing some code recently and decided to really give “impl Trait in arguments” a good trial. It has so far seemed very nice for taking in convertible arguments like

fn new(tag: impl Into<Cow<'static, str>>) -> Self

I hit one more case where it seems it might be possible to provide some good readability sugar, but currently cannot be expressed; generic type arguments in trait implementations. As an example, currently you can write:

struct Foo(String);

impl<S: Into<String>> From<S> for Foo {
    fn from(s: S) -> Self {
        Foo(s.into())
    }
}

instead it may be possible to support something like

impl From<impl Into<String>> for Foo {
    fn from(s: ?????) -> Self {
        Foo(s.into())
    }
}

but, as you can see this runs into an issue with having the type named in multiple places.

I was wondering if anyone knows of any existing discussion around a feature like this, or whether there’s been discussion of extensions to the “impl Trait in arguments” feature that might help this nameability issue?


#2

impl Trait as sugar for generics is targeted at the common case where a function has a single use site for the generic parameter, therefore it doesn’t need a name. If you use something in two places, then you need a name, so it doesn’t make much sense to want impl Trait which is an anonymous type.


#3

Right, which is why it doesn’t extend directly to this use case. But conceptually From only cares about the type once, it just happens to need to mention it in two places because of how traits work.


#4

From does care about the type twice :slight_smile:

Let’s say you did:

String::from(foo)

There are several type variables at play in this expression:

<String as From<?0>::from(foo: ?1)

Based on the signature of From, we know that ?0 and ?1 must be the same type. By inferring the type of the foo variable, we can infer the type of ?0, and therefore know which From impl this method should be dispatched to.


Another way to look at the situation is that we’ve decided that impl Trait inserts a type parameter on the nearest scope - in this case it would be on the from function. What you want it seems is to inject the parameter further out, on the trait.


#5

This is what I mean when I say it only really cares about the type once, there’s a single point that defines the generic type parameter, and a single point that uses the generic type parameter. Exactly the same as the use case for impl Trait in function arguments. As I see it From is very close to being some sort of nominal trait alias for for<T> Fn(T) -> Self; maybe this only makes sense with my personal understanding of the type system.

This is a great way to look at it.


I really don’t know if there’s any kind of sugar that would make sense here, or extend well to other traits, it was just something I noticed while trying out universal impl Trait.


#6

Yes you’re right, but by that argument we should be allowed to write:

impl From<impl Into<String>> for Foo {
    fn from(s: _) -> Self {
        Foo(s.into())
    }
}

Eliding the type of s since it’s completely determined by the impl header and the trait definition. But that would be confusing, sometimes redundancy is good.


#7

There’s sort of a similarity with this to the “associated type inference” thing, where you could write

impl Iterator for MyIter {
     fn next(&mut self) -> Option<i32>
}

And we figure out that Item = i32.

There’s also been the option of just omitting types from function signatures in impls entirely, which has been proposed.

There’s a larger question about how much flexibility / inference we should allow in impls.


#8

Maybe the thing we actually want is to be able to name the type indirectly, by making it possible to refer type parameters in the same manner as associated types. This sidesteps unresolved questions about inference until later (and personally, I’m not a huge fan of fn from(s: _)).

For example, given trait From<T> {..}, we would write

impl From<impl Into<String>> for Foo {
    fn from(s: Self::T) -> Self {..}
}

This solution has two downsides, though:

  • We’d need to abandon using T and other single-letter type variables for the purposes of clarity (<Foo as From<String>>::T does not strike me as something we want to encourage [1]). Attempting to fix this just in std is going to open an enormous can of bikeshedding worms.
  • Crates need to commit to their type variables’ names, since they’d now be part of their public interface. AFAIK changing the name of a type parameter is currently not a breaking change.

[1] Yes, I’m aware that this syntax is just an obfuscated way of saying String, but that’s missing the point.


#9

I just want to say that I like the current solution that uses type arguments because it keeps the information local. Would ergonomics really improve if we omitted them somehow in the impl? I think reasoning about code would be more difficult because of the introduced indirection. The current solution is very direct because each type argument ties every occurrence of the same type nicely together. There’s no need to look at another file that contains the trait definition to understand what’s happening.


#10

Yes, yes it would. Coming from Haskell (which invented type classes or “traits”), where you don’t need to say the type of a function in the definition of a type-class method instance, repeating the type in a trait feels like a paper cut to me every day. Also, there are years of experience of not annotating the type in Haskell instances, and readability has been doing just fine.