Default parameter mechanics

Have you ever wondered why these don't work...

use std::collections::HashSet;
// default parameter   vvvvvvvvvvvvvvv
// struct HashSet<Key, S = RandomState> { .. }

// default   vvvvvvvvvv
pub enum Foo<T = String> {
    Bar(T),
    Baz,
}

fn main() {
    let mut hs = HashSet::default();
    hs.insert(String::new());

    let mut hs = HashSet::<_, _>::default();
    hs.insert(String::new());
    
    let foo = Foo::Baz;
    let foo = Foo::<_>::Baz;
}

...whereas these do work?

    let mut hs = HashSet::<_>::default();
    hs.insert(String::new());

    let mut hs = HashSet::new();
    hs.insert(String::new());

    let foo = <Foo>::Baz;

Well have I got the guide for you!

(TL;DR)

12 Likes

Thanks a lot! :slight_smile: But I don't understand the explaination why this works:

let mut hs = HashSet::<_>::default();
hs.insert(String::new());

... and this doesn't:

let mut hs = HashSet::<_, _>::default();
hs.insert(String::new());

You've written:

The key difference here is that if no required parameters are specified, then all the type (and const) parameters -- including defaulted parameters -- are filled in with inference variables. But if one or more non-lifetime parameter is specified, it desugars to a qualified type path -- where default parameters act the same as they do in type ascription.

[...]

As is clear from the example, using _ explicitly counts as specifying a type parameter. Also note that the desugaring to "all parameters are inference variables" only happens when the type is not inside <>s.

When HashSet::<_> counts as one or more non-lifetime parameter why doesn't it HashSet::<_, _>?

1 Like

The way it's currently modeled in my mind is:

  1. _ in Rust is often read as "I don't care", and it indicates (among other things) a place where a type is inferred. Important here is that type inference is a function-local process.
  2. Then from 1. I then infer that using _ in a place where a default type would have been used overrides the default with the imperative to infer it.

The thing is, in the HashMap example, what could possibly provide the state type given the containing fn? There isn't anything to derive the type from! And thus type checking fails.

2 Likes

I see! Makes sense to me. :slight_smile: Thanks!

It does, but because _ doesn't fall back to the default parameter type when inference fails, the whole thing fails.

These all act the same (and fail):

let mut hs = HashSet::default();
let mut hs = HashSet::<_, _>::default();
let mut hs = <HashSet<_, _>>::default();
let mut hs: HashSet<_, _> = Default::default();

Whereas you want something where eliding the (second) parameter means "use the default". That's any of these (which all act the same).

let mut hs = HashSet::<_>::default();
let mut hs = <HashSet<_>>::default()
let mut hs: HashSet<_> = Default::default();

(Or you can be explicit, e.g. HashSet::<_, RandomState>::default().)

I'll see if I can find a good place to put in a reminder that _ doesn't fall back to defaults and thus HashSet::<_, _> isn't what you want.

3 Likes

Thanks! It is comforting to know that there is at least someone who knows this stuff, but in spite of your heroic and noble efforts, sadly I will never be joining you, I will have to just blunder on making guesses. Even if I managed to understand the topic briefly, a week would pass and I would forget it all again!

[ If I was 50 years younger it might be different ]

2 Likes

Ahh I read it wrong.^^ I understood the text as if for HashSet::<_, _> the second _ should also act as the default parameter here because due to mentioning the parameters explicitly the qualified type path / type ascription behaviour case kicks in and therefore the default parameter should be used for the second generic parameter but that's obviously not the case and you don't even say that it should be like that. :smiley: