Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more content to impl-trait.md #1017

Merged
merged 8 commits into from
May 24, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 103 additions & 16 deletions src/types/impl-trait.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,120 @@
>
> _ImplTraitTypeOneBound_ : `impl` [_TraitBound_]

## Anonymous type parameters
`impl Trait` provides ways to specify unnamed but concrete types that
implement a specific trait.
It can appear in two sorts of places: argument position (where it can act as an anonymous type parameter to functions), and return position (where it can act as an abstract return type).

```rust
trait Trait {}
# impl Trait for () {}

> Note: This section is a placeholder for more comprehensive reference
> material.
// argument position: anonymous type parameter
fn foo(arg: impl Trait) {
}

// return position: abstract return type
fn bar() -> impl Trait {
}
```
## Anonymous type parameters

> Note: This is often called "impl Trait in argument position".
(The term "parameter" is more correct here, but "impl Trait in argument position" is the phrasing used during the development of this feature, and it remains in parts of the implementation.)

Functions can declare an argument to be an anonymous type parameter where the
callee must provide a type that has the bounds declared by the anonymous type
parameter and the function can only use the methods available by the trait
bounds of the anonymous type parameter.
Functions can use `impl` followed by a set of trait bounds to declare a parameter as having an anonymous type.
The caller must provide a type that satisfies the bounds declared by the anonymous type parameter, and the function can only use the methods available through the trait bounds of the anonymous type parameter.

They are written as `impl` followed by a set of trait bounds.
For example, these two forms are almost equivalent:

## Abstract return types
```rust,ignore
trait Trait {}

// generic type parameter
fn foo<T: Trait>(arg: T) {
}

// impl Trait in argument position
fn foo(arg: impl Trait) {
}
```

> Note: This section is a placeholder for more comprehensive reference
> material.
That is, `impl Trait` in argument position is syntactic sugar for a generic type parameter like `<T: Trait>`, except that the type is anonymous and doesn't appear in the [_GenericParams_] list.

> **Note:**
> For function parameters, generic type parameters and `impl Trait` are not exactly equivalent.
> With a generic parameter such as `<T: Trait>`, the caller has the option to explicitly specify the generic argument for `T` at the call site using [_GenericArgs_], for example, `foo::<usize>(1)`.
> If `impl Trait` is the type of *any* function parameter, then the caller can't ever provide any generic arguments when calling that function.
This includes generic arguments for the return type or any const generics.
>
> Therefore, changing the function signature from either one to the other can constitute a breaking change for the callers of a function.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see how changing from generic parameter to impl trait would be a breaking change, but can you explain how the reverse is also a breaking change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was based on text that I copied from the edition guide. For the reverse direction, I'm not especially familiar with using turbofish when there are multiple type parameters, e.g., what if we went from

fn foo<T: PartialOrd>(a: T, b: impl PartialOrd) {}
foo::<i32>(0, 1.0);

to

fn foo<T: PartialOrd, U: PartialOrd>(a: T, b: U) {}
foo::<i32>(0, 1.0);

Is it valid to omit the second type parameter? How does the compiler know which one is omitted? (I can't find any text in the reference about this case.) I'm sorry if this is a bad example.

Copy link
Contributor Author

@tlyu tlyu May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I experimented a bit in the playground, and it seems like in practice:

  • If there are multiple type parameters, either all or none must be specified by turbofish
  • If there is a mixture of type parameters and impl Trait in argument position, turbofish may not be used for any type arguments at all (edit: apparently this includes type parameters for the return type!)

Side note: are we avoiding using the term "turbofish" in the reference?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. The second parameter can be omitted if it is defaulted, like <T, U=T>, but that can't be done with functions.

The reference normally doesn't discuss semver compatibility since that is a cargo concept (documented here). It's probably fine to leave this as-is, though.

There is one mention of turbofish, plus another in the glossary, but otherwise it usually doesn't come up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some further experimentation suggests that both my proposed text and the edition guide are differently wrong:

  • impl Trait in return position doesn't prevent the use of turbofish, contrary to the edition guide, which says that "In the case of impl Trait, if it is used anywhere in the function definition, then you can't use turbo-fish at all"
  • impl Trait in any parameter type prevents the caller from providing any generic arguments at all, including return type and even const generics (No way to use fn with both const generics and impl Trait rust#85475)! This is contrary to my text, which says it only prevents turbofish for that parameter.


## Abstract return types

> Note: This is often called "impl Trait in return position".

Functions, except for associated trait functions, can return an abstract
return type. These types stand in for another concrete type where the
use-site may only use the trait methods declared by the trait bounds of the
type.
Functions can use `impl Trait` to return an abstract return type.
These types stand in for another concrete type where the caller may only use the methods declared by the specified `Trait`.
Each possible return value from the function must resolve to the same concrete type.

`impl Trait` in return position allows a function to return an unboxed abstract type.
This is particularly useful with [closures] and iterators.
For example, closures have a unique, un-writable type.
Previously, the only way to return a closure from a function was to use a [trait object]:

```rust
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
```

This could incur performance penalties from heap allocation and dynamic dispatch.
It wasn't possible to fully specify the type of the closure, only to use the `Fn` trait.
That means that the trait object is necessary.
However, with `impl Trait`, it is possible to write this more simply:

```rust
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
```

which also avoids the drawbacks of using a boxed trait object.

Similarly, the concrete types of iterators could become very complex, incorporating the types of all previous iterators in a chain.
Returning `impl Iterator` means that a function only exposes the `Iterator` trait as a bound on its return type, instead of explicitly specifying all of the other iterator types involved.

### Differences between generics and `impl Trait` in return position

In argument position, `impl Trait` is very similar in semantics to a generic type parameter.
However, there are significant differences between the two in return position.
With `impl Trait`, unlike with a generic type parameter, the function chooses the return type, and the caller cannot choose the return type.

The function:

```rust,ignore
fn foo<T: Trait>() -> T {
```

allows the caller to determine the return type, `T`, and the function returns that type.

The function:

```rust,ignore
fn foo() -> impl Trait {
```

doesn't allow the caller to determine the return type.
Instead, the function chooses the return type, but only promises that it will implement `Trait`.

## Limitations

They are written as `impl` followed by a set of trait bounds.
`impl Trait` can only appear as a parameter or return type of a free or inherent function.
It cannot appear inside implementations of traits, nor can it be the type of a let binding or appear inside a type alias.

[closures]: closure.md
[_GenericArgs_]: ../paths.md#paths-in-expressions
[_GenericParams_]: ../items/generics.md
[_TraitBound_]: ../trait-bounds.md
[trait object]: trait-object.md
[_TypeParamBounds_]: ../trait-bounds.md