Named type parameters

A recent comment on the "partial turbofish" RFC by @nikomatsakis started a discussion on named type parameters of types and traits and how the current situation does not scale. An excerpt:

I have been historically quite grumpy with how our defaults in type parameters in any case. In particular, I don't like them being a linear list. I want to give names. Consider HashMap:

pub struct HashMap<K, V, S = RandomState>

What happens if we want to add an allocator parameter (say) to HashMap? Now, in order to specify the allocator, I have to specify the S parameter too? That seems dumb. This now seems sort of connected to optional parameters in functions, of course.

This gave me an idea for a solution:

pub struct HashMap<K, V, S = RandomState, A = Heap> { .. }

let foo: HashMap<K, V, {A} = OtherAlloc> = HashMap::default();
// variations with different sigils:
let foo: HashMap<K, V, [A] = OtherAlloc> = HashMap::default();
let foo: HashMap<K, V, |A| = OtherAlloc> = HashMap::default();
let foo: HashMap<K, V, #A = OtherAlloc> = HashMap::default();

Here, {A} refers to name A as used in HashMap<K, ..> and I think it looks pretty good.

If we can infer K, V then we can also write:

let foo: HashMap<{A} = OtherAlloc> = HashMap::default();
// ..

To which, in response, @comex proposed:

HashMap<K, V, A: OtherAlloc>

but said that:

Might be easy to confuse with trait bounds, but that’s not so different from the similarity between a struct declaration, struct Foo { a: T } and the syntax for a value, Foo { a: val }.

I'm extracting out that discussion now and hoping that we can find a good solution together on this forum so that we can start an RFC about it eventually.

5 Likes

This solves a part of the problem. To me the problem is the style of using those single-character identifiers for type parameters in the first place. For example, some tokio crate consistently use R, S, E, and took some time for me to figure out that S was for Response (which would otherwise clash with the R for Request). We don’t yet have a culture of using better names and/or consistently documenting what the type parameters signify and it’s easy to forget, as an author/maintainer, that the letters you pick may be not at all instinctive to other people.

I filed an issue for the API guidelines at some point, but has not seen a response in the past 8 months.

Oh yes, if we had named type parameters, then we'd need better names such as:

struct HashMap<Key, Value, BHasher = RandomState, Alloc = Heap> { .. }

We will need to do this with most of the standard library.

I think the problem of bad naming is that many programmers don't like to name things (myself included) and are lazy about such things. This is difficult to change but could be improved with linting perhaps based on parameter ident length?

Your comment also sparked a thought: Since named type parameters become part of the public API and can't be changed after, perhaps there needs to be some way of opting into naming (or opting out)? - otherwise, named type parameters can become a large source of breakage.

1 Like

Being dumb: why not

let foo: HashMap<A = OtherAlloc> = ...

?

3 Likes

Because Thing<X=Y> already means Thing where Self::X == Y.

(Think Box<Iterator<Item = u8>>)

1 Like

I guess this could conflict with syntax for bounds on associated types?

(Which is quite unfortunate anyway; where <T as Iterator>::Item = U would be much clearer than where T: Iterator<Item=U>, even if it's more typing.)

1 Like

Is @nikomatsakis concern regarding places without inference such as signatures or places with inference such as turbofish? Or both? In places without inference I see the issue that we currently do not allow something like HashMap<i32, i32, _, MyAlloc> but we could just allow that with _ using the default.

I don’t see the status quo as a problem, at least there is insufficient motivation for changing it at the moment. Inference and defaults can go a long way in mitigating this.

Yes, the problem is with associated types per @kennytm’s comment.

We don't? Interesting, TIL...

I agree it would go some ways to making the situation more ergonomic... but what if you only want to change the allocator and let the key and the value type be inferred? Then you have to write: HashMap<_, _, _, MyAlloc> which is somewhat outrageous...

2 Likes

The allocator value must be provided upon construction, it would be very difficult for it to go uninferred.

Oh yes, good point; However, there may be other APIs where this is not the case.

True, but couldn't that syntax support both?

No because this is legal:

trait X<A=i32> {
  type A;
}
type M = X<A=u32>; // which A are you talking about?
1 Like

If there were a way to opt into named type parameters, though, as @Centril suggested above, then the name collision could be a hard error without breaking backward compatibility.

So I've been thinking that you can do:

trait X<{A} = i32> { // Using the syntax {A} opts in
    type A;
}

trait M = X<{A} = u32>; // This refers to the type parameter A
trait M = X<u32>; // Same as previous.
trait M = X<{A} = u32, A = ()>; // Type param is u32 assoc type is ()
trait M = X<u32, A = ()>; // Same as previous

This makes the opting in syntax the same as the syntax for using the name of the type parameter which is always nice property to have. The brackets used in {A} are totally bikesheddable.

To be honest, I don’t see the {TypeParam} syntax gaining traction. It’s just too weird: think of all the people learning Rust and seeing that for the first time, and finding out that syntax is only required because of some obscure backward compatibilty issue.

But doesn’t the example that @kennytm posted – the one with an associated type and type parameter with the same name – seem like an anti-pattern? If that’s the only thing keeping us from getting the syntax we want, then we should consider deprecation.

What we could do is, initially, just default to the associated type when there is a conflict, but add a warning (on the trait definition itself, and wherever Trait<A=u32> syntax is used), and at some later date, turn the trait definition warning into a hard error.

2 Likes

It's just a placeholder/strawman syntax until we get something better =)

We should check how often it happens that an associated item and a type param have the same name by making it an error for them to have the same name, and then see how much a problem it is with a crater run. Since an epoch is approaching, it might be prudent to do this soon-ish.

I was thinking the same thing :slight_smile: I'll try and open a PR with that change soon so we can do the crater run

2 Likes

I'd avoid {...} here since that's used for const generic parameters to flip into expression mode from type mode.

2 Likes

I think that’s fine — we can define shadowing order. If you use the same name for both (which is silly and who would do that?), then you just won’t be able to use named arguments.