Is the empty turbofish just for asthetics?

Dear turbofish appreciation society,

I appreciate this may be a delicate topic, but is there any use for an empty turbofish?

E.g.

fn hello_back() -> String {
    "hello".chars().rev().collect::<>()
}

It seems totally valid code which I wasn't expecting. I could understand that: "hello".chars().rev().collect::<_>() should be valid, but empty? If there's no valid reason why someone should write an empty turbofish then maybe it should be turned into a warning given the wrong number of arguments are specified?

Or is it purely for asthetic purposes that people think it looks pretty?

Hmm, that's curious.

It's absolutely expected that you can put empty generic parameter lists on non-generic things, like

fn hello_back<>() -> String<> { "hello".to_string() }

because that's helpful for macros and other generated code.

But empty-on-something-that-actually-has-some is not something I knew worked.

6 Likes

::<> makes the first statement here not compile: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4fc7286417d85f51648a65f10382ef0a

#[derive(Debug, Default)]
struct Foo<T = u32, U = u64>(T, U);

fn main(){
    dbg!(Foo::<>::default());
    dbg!(<Foo>::default());
    dbg!(Foo::<()>::default());
}
error[E0282]: type annotations needed
 --> src/main.rs:5:10
  |
5 |     dbg!(Foo::<>::default());
  |          ^^^^^^^^^^^^^^^^ cannot infer type for type parameter `T` declared on the struct `Foo`

Also, rustfmt removes the ::<> in that statement when ran.

1 Like

From @matt1985's post it does look like <> has the semantics of <..>, if that were valid syntax: infer / use a placeholder parameter for each generic parameter (which is thus not the same as writing no turbofish, which gets to use default type parameters when available).

That being said, I find that behavior (on top of unintuitive) to be inconsistent and cause problems with some macros. Take, for instance, the following crate (of mine):

It works as follows: you #[with]-annotate a function definition, say foo<T>, and it converts that into a with_foo<R, F, T>. Then, at call-site / usage-site, you also #[with]-annotate a foo $(::<…>)? call, and it converts that into a with_foo $(::<_, _, …>)? to make sure the two added type parameters are inferred.

The fact that one can feed an "empty parameter sequence" into a turbofish site to express that all parameters should be inferred means that I now must special-case that pattern too (hence why it is inconsistent: it requires special-casing) to become:

foo $(::<$(…)?>)? -> with_foo $(::<$(_, _, …)?>)?

Granted, it's definitely a manageable inconsistency, but an inconsistency nonetheless. Could this kind of oversight(s) be removed in a future edition?

  • That being said, it does bring into the table the topic of a syntax to explicitly elide all "remaining" (type?) parameters, which I think could be desirable in general (it could, for instance, make a fn generic_with_anon_type_param<T> (f: impl …) be fed a T at call site: generic_with_anon_type_param<T, ..> without requiring having solved the "can anonymous type parameters be explicitly fed" question).
4 Likes

Yes. This is frontend syntax only, not anything impacting "core rust", so it could be changed across an edition boundary.

If someone were interested in doing that, the first step would be to make a lint warning about it.

(Said without making an official statement on what should happen, though at least a lint about it seems plausible to me.)

2 Likes