What is Rust's turbofish

Have you ever heard about the “turbofish”? It is that piece of Rust syntax that looks like ::<SomeType>. In this post I will describe what it does and how to use it.

First of, if you were to write something like this:

fn main() {
    let numbers: Vec<i32> = vec![
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    ];

    let even_numbers = numbers
        .into_iter()
        .filter(|n| n % 2 == 0)
        .collect();

    println!("{:?}", even_numbers);
}

The compiler would yell at you with the following message:

$ cargo check
    Checking blog-post v0.1.0 (/Users/davidpdrsn/Desktop/blog-post)
error[E0282]: type annotations needed
 --> src/main.rs:6:9
  |
6 |     let even_numbers = numbers
  |         ^^^^^^^^^^^^
  |         |
  |         cannot infer type
  |         consider giving `even_numbers` a type

error: aborting due to previous error

For more information about this error, try `rustc --explain E0282`.
error: Could not compile `blog-post`.

To learn more, run the command again with --verbose.

What this message says is that it doesn’t know what type you’re trying to “collect” your iterator into. Is it a VecHashMapHashSet, or something else that implements FromIterator?

This can be fixed in two different ways. Either by declaring the type of even_numbers when you declare the variable:

let even_numbers: Vec<i32> = ...

Or by using a turbofish:

let even_numbers = numbers
    .into_iter()
    .filter(|n| n % 2 == 0)
    .collect::<Vec<i32>>();

The ::<Vec<i32>> part is the turbofish and means “collect this iterator into a Vec<i32>”.

You can actually replace i32 with _ and let the compiler infer it.

let even_numbers = numbers
    .into_iter()
    .filter(|n| n % 2 == 0)
    .collect::<Vec<_>>();

The compiler is able to do that because it knows the iterator yields i32s.

With this change our final program looks like this:

fn main() {
    let numbers: Vec<i32> = vec![
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    ];

    let even_numbers = numbers
        .into_iter()
        .filter(|n| n % 2 == 0)
        .collect::<Vec<_>>();

    println!("{:?}", even_numbers);
}

Now the compiler is happy:

$ cargo check
    Checking blog-post v0.1.0 (/Users/davidpdrsn/Desktop/blog-post)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s

When the turbofish can be used

Lets look at another, similar example:

fn main() {
    let s = "Hello, World!";
    let string = s.into();
}

Compiling this gives us an error similar to what we had before:

$ cargo check
    Checking blog-post v0.1.0 (/Users/davidpdrsn/Desktop/blog-post)
error[E0282]: type annotations needed
 --> src/main.rs:3:9
  |
3 |     let string = s.into();
  |         ^^^^^^
  |         |
  |         cannot infer type
  |         consider giving `string` a type

error: aborting due to previous error

For more information about this error, try `rustc --explain E0282`.
error: Could not compile `blog-post`.

To learn more, run the command again with --verbose.

We might assume that we fix this by using the turbofish again like so:

fn main() {
    let s = "Hello, World!";
    let string = s.into::<String>();
}

However that gives us a new error:

$ cargo check
    Checking blog-post v0.1.0 (/Users/davidpdrsn/Desktop/blog-post)
error[E0107]: wrong number of type arguments: expected 0, found 1
 --> src/main.rs:3:27
  |
3 |     let string = s.into::<String>();
  |                           ^^^^^^ unexpected type argument

error: aborting due to previous error

For more information about this error, try `rustc --explain E0107`.
error: Could not compile `blog-post`.

To learn more, run the command again with --verbose.

When I was still new to Rust this error confused me a lot. Why does ::<> work on collect but not on into? The answer is in the type signatures of those two functions.

collect looks like this:

fn collect<B>(self) -> B 

And into looks like this:

fn into(self) -> T

Notice that collect is written as fn collect<B> and into is written as just fn into.

The fact that collect has a generic type <B> is what allows you to use ::<>. Had into somehow been written as fn into<B> then you would have been able to write .into::<String>(), but since it isn’t, you can’t.

If you encountered a function like fn foo<A, B, C>() then you would be able to call it like foo::<String, i32, f32>().

The turbofish can also be applied to other things such as structs with SomeStruct::<String>::some_method(). This will work if the struct is defined as struct SomeStruct<T> { ... }.

You can almost think of the things inside ( and ) to be the “value arguments” to a function and the things inside ::< and > to be the “type arguments”.

So to fix our code from above we would have to declare the type when declaring the variable:

fn main() {
    let s = "Hello, World!";
    let string: String = s.into();
}

And again the compiler is now happy:

$ cargo check
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s

We can technically also use the fully qualified trait syntax:

fn main() {
    let s = "Hello, World!";
    let string = <&str as Into<String>>::into(&s);
}

Or optionally

fn main() {
    let s = "Hello, World!";
    let string = Into::<String>::into(s);
}

I would personally just use give s a type check declaring the variable in this case.

I recommend you check out the the book if you want to learn more about how generics work in Rust.

By the way after some digging I found this reddit comment to be the origin on the word “turbofish”.

posted @ 2024-03-09 12:09  RioTian  阅读(14)  评论(0编辑  收藏  举报