`impl Trait` Arg Syntax is Equivalent to Bounded Generic Arguments, Right?

I just wanted to make sure that I am understanding the impl Trait syntax in function arguments correctly. These two examples are equivalent correct?

fn do_something(writer: impl Write) {
    // Something...
}
fn do_something<T: Write>(writer: T) {
    // Something...
}

The impl Trait syntax is just a less verbose way to write the generic type parameters out right?

1 Like

yes, they're equivalent

1 Like

Sweet, thanks.

They are not completely equivalent. impl Trait in argument position cannot be explicitly instantiated, i.e. do_something::<i64>(0) doesn't work with it.

3 Likes

Got it. That's because there isn't technically a generic argument defined that could be put in the "turbofish" ( ::<> ).

Yes, that's right, it's not syntactically explicit.

1 Like

eek TIL that's almost c++ levels of confusing

i guess i now understand why impl X for arguments is pretty much never used

2 Likes

IMO argument position impl Trait has really no added value. There's nothing it can do that already-existing generic syntax couldn't, but the converse is true, so it's strictly less useful than explicit generics.

The conventional wisdom behind allowing it in argument position was that "it's consistent", but that seems misguided because it's not even allowed in return position of trait methods, nor is it allowed in a ton of other places, for instance you can't make an impl Trait-typed variable.

1 Like

There is definitely intent to make both of these work, though, and IIRC existentials are going to be finalized as type Foo = impl Trait.

2 Likes

The impl Trait syntax is more "plain English" so to speak, as it tells you exactly what it is doing. It is taking as an argument anything that implements Trait. In that way I like it. On the other side, it is hiding more of what it is doing under-the-hood which can make things more confusing when you happen to get mixed up in what is actually happening for some reason.

That is kind of the nature of what Rust does all over the place. It elides lifetimes, it automatically dereferences pointers. Even when you do fn do_something<T>(hello: T) it is implied that T: Sized.

It is a difficult balancing act between hiding details to make it more readable, and showing details to avoid confusion when the details do matter.

1 Like

Both generics and impl objects are important:

  • Generics will allow you to:
    • Use associated types:
      fn foo<T: Iterator>(mut val: I) {
          let my_val: T::Item = val.next().unwrap();
      }
      
    • Use turbofish and lifetimes (mixed too):
      fn foo<'a, T, B: 'a>() {}
      foo::<'_, _, &'b [u8]>();
      
  • impl Trait will allow you to:
    • Hide the name of the return type of the function to the caller to not expose ugly types, or private items:
      fn foo() -> impl Debug {0usize}
      let x: ??? = foo();
      
    • Use generics without the generics syntax:
      fn foo_1(_x: impl Debug) {}
      fn foo_2<'a>(_x: impl Debug + 'a) {} //You can combine them
      
  • Dynamic dispatch will allow you to:
    • Hide the underlying type, even from the compiler.
    • Swap between types at runtime:
      let mut x: &dyn Debug = &0;
      x = "abc";
      x = &();
      
1 Like

i like impl X syntax, it's just that, given that there's two almost equivalent constructs, of which one is a little more powerful (allowing for the turbofish syntax), it suddenly feels like using it is just laziness and a little more typing with <T: Write> gives users of the function more flexibility

1 Like