Mental model for foo::<bar>::baz syntax?

I've used other languages with generic types. I always thought of it as similar to a function call, passing arguments to an abstraction. So:

some_function(a, b)

Creates an "instance" (a call) of some_function by passing specific arguments a and b.

SomeType(a, b)

Creates an instance of the class/type SomeType, calling its constructor with arguments. Not all languages have this exact syntax, but some do (Kotlin, Swift, Python, C++). And lastly:

HashTable<Int, String>

Creates an instance of a type, by passing two arguments to a generic type. All three share this idea of applying/calling an abstraction with arguments. In the last, the () changes to <>, and the application there is a compile-time thing, not a runtime call.

I understand the problems with parsing that last one, because < and > are reused for comparison operators. Rust's syntax appears to be:

HashTable::<i32, String>

This confuses me because :: usually means to me that we're drilling down in a path of some kind. Should I still think of that as "the type HashTable<i32, String>"? So ::< ... > is simply how you "apply/call/instantiate" the generic type and pass it arguments. Or should I think of :: as some kind of path drilling?

I would argue that HashTable<Int, String> is the "real" Rust syntax. Doing a quick grep through some Rust repositories I have open, Foo<Bar> syntax appears about 5 times more often than Foo::<Bar> syntax.

I think of the turbofish syntax as merely a variant of the normal Foo<Bar> syntax. This variant exists purely to avoid the parsing ambiguity that you mentioned. It is required only in paths in expressions.

You could think of the :: in this syntax as a signal to the parser (and the reader) that the preceding tokens are part of a path/type rather than an expression on their own. Or just think of ::< as an alternate, disambiguated form of the < bracket.

HashTable::<i32, String>
This syntax just exists due to parsing simplicity reasons iirc, its not due to any other internally-consistent logic. You shouldn't think of it as anything beyond "I need to put :: in this one weird case". You should think of it as the same (logically) as HashTable<K,V>.

Reminder that if you prefer to not write the turbofish-::, you can just put the parser into type mode directly instead: <HashMap<i32, String>>::new() works great.

The problem is that in expression mode x < b is a comparison, so the ::< is needed to distinguish.

Like others have said, I definitely wouldn't overthink it. It's just a syntax thing.

...that saaaaiid, you could think of a generic type as a namespace that contains all of its variants:

mod MyThingA {
    pub struct I32 {
        item: i32
    }
    
    impl I32 { pub fn new(item: i32) -> Self { Self { item } } }
    
    pub struct I64 {
        item: i64
    }
    
    impl I64 { pub fn new(item: i64) -> Self { Self { item } } }
}

struct MyThingB<T> {
    item: T
}

impl<T> MyThingB<T> { fn new(item: T) -> Self { Self { item } } }

fn main(){
    let a32 = MyThingA::I32::new(12);
    let a64 = MyThingA::I64::new(12);
    let b32 = MyThingB::<i32>::new(12);
    let b64 = MyThingB::<i64>::new(12);
}

I think that mental model struggles when handling the combinatorial complexity of having multiple generic arguments, especially when you start getting into trait bounds and nested generics (even if it is essentially how monomorphization works). Though it might be interesting to see a language that did take that approach...

Just ignore it in your mental model, honestly. Rust is “an ML descendant in an C++ trenchcoal”. And C++ grammar is quite literally undecidable.

Rust developers haven't liked to have a language where you couldn't even answer a simple question “is this syntactically valid piece of code or not” with 100% certainty. Thus turbofish was added to resolve the use.

That's all there to it, no deep meaning.

Note that that article is about grammar, not syntax. The compiler can reject syntactically invalid code with 100% certainty in finite time; but once you get into mono, then you enter the realm of undecidability. The same is true of Rust.

Not in C++. Precisely because of lack of turbofish and difference between generics and templates.

What's the difference? A grammar does not describe the meaning of the strings—only their form.

The problem with C++ is that to know whether foo<bar> is template foo with parameter bar or unfinisihed expression that needs baz to become foo<bar>baz (interpreted as (foo < bar) > baz one needs to know whether foo is a template or not. And because templates are not generics one may need to execute arbitrary code before one would know if quz::foo is template or not (thanks to the constexpr calculations).

Rust sidesteps all that madness by simply making foo::<bar> distinct from foo<bar>baz on the trivially verifyable level.

Right. But in Rust one may first parse the whole program, build an AST tree and then start “getting into mono”. Few ambiguities are carefully selected to make sure building AST nodes with mark “could be foo or bar” is feasible. But in C++, thanks to the lack of turbofish, you have no such luxury: you have to execute arbitrary C++ code during the constructions of AST!