Impl Trait Parameters And Turbofish
Agenda
We will overview the desugaring of impl Trait
, how turbofish syntax works and the limitation of using them both together.
If you want to skip to the limitation itself, then go to "Turbofish Limitation Aftermath".
βThis post covers only
impl Trait
in function parameters position.
impl Trait
in function return types is a totally different concept, that has almost nothing to do with what's in this article.
Impl Trait Desugaring
Rust allows you to have a shortcut for defining a function with generic parameters bounded by trait and lifetimes expression.
Here is an example of impl Trait
usage in function parameter types:
#![allow(unused)] fn main() { trait Trait1 {} trait Trait2 {} fn foo<T>( a: impl Trait1, b: T, c: impl Trait1 + Trait2 + 'static, d: impl Trait1, ) { /**/ } }
Under the hood this probably desugars into the following code:
The word probably here denotes that it is not the exact desugaring that
rustc
does, which is it's private implementation detail.
#![allow(unused)] fn main() { trait Trait1 {} trait Trait2 {} fn foo< T, __T1: Trait1, __T2: Trait1 + Trait2 + 'static, __T3: Trait1 >( a: __T1, b: T, c: __T2, d: __T3, ) { /**/ } }
Here we have a regular generic parameter T
, and three more generic parameters generated for us by impl Trait
syntax automatically (__T1
, __T2
, __T3
).
Even if the function uses the same impl Trait
in several function parameters, they still generate different generic parameter types. That's why even though a
and d
have impl Trait1
type annotation, they still use different __T1
and __T3
generic parameters in their desugaring shown above.
The symbols __T1
, __T2
, __T3
are not available in Rust code, therefore there will be limitations when working with them described in the following paragraphs.
Type Inference And Turbofish Syntax
Let's recap what turbofish (::<>
) syntax provides to us.
Take an example generic function.
#![allow(unused)] fn main() { fn bar<T, U>(a: T, b: U) { /**/ } }
Suppose we want to provide it with T = bool
and U = i32
. We can do this in various ways.
#![allow(unused)] fn main() { fn bar<T, U>(a: T, b: U) { /**/ } bar (false, 99); // infer all generic params bar::<bool, i32>(false, 99); // specify all generic params explicitly with turbofish bar::<_, _> (false, 99); // infer 2 generic params bar::<_, i32> (false, 99); // infer the first param, but specify the second bar::<bool, _> (false, 99); // specify the first param, but infer the second }
Generic parameters in type definitions and type aliases can use default values. It is not possible to set default values for functions though!
#![allow(unused)] fn main() { enum Baz<A, B, C = u32> { A(A), B(B), C(C), } // Now we can create the value of the enum as such: Baz::A::<_, ()> (false); // (1) Baz<bool, (), u32> Baz::B::<String, _> (false); // (2) Baz<String, bool, u32> Baz::C::<bool, u32, _>(false); // (3) Baz<bool, u32, bool> }
By omitting the third argument in the turbofish syntax in cases (1) and (2) we opted in to using the default u32
type for the generic parameter C
.
When the third generic parameter is overridden, even with a wildcard (_
), it means that the default value is ignored. The wildcard merely specifies that the generic type parameter has to be inferred from usage.
It means, that the following will produce a compile error.
#![allow(unused)] fn main() { enum Baz<A, B, C = u32> { A(A), B(B), C(C), } Baz::C::<bool, u32>(false); // ^^^^^ expected `u32`, found `bool` }
This is because when using an explicit turbofish syntax all required type parameters must be explicitly specified and the optional ones will be set to their default values. Even, if we want to specify only the required type parameters, but infer the rest, we have to use a wildcard _
to do that.
The same is true even if part of the required type parameters can be inferred. For instance, we can't use this syntax to have the value type of the HashMap
inferred.
#![allow(unused)] fn main() { use std::collections::HashMap; let map = HashMap::<String>::from_iter([ // | ^^^^^^^ ------ supplied 1 generic argument // | | // | expected at least 2 generic arguments ("key".to_owned(), true) ]); }
We are forced to enumerate all remaining deduced generic parameters with _
.
#![allow(unused)] fn main() { use std::collections::HashMap; let map = HashMap::<String, _>::from_iter([("key".to_owned(), true)]); }
Turbofish Limitation Aftermath
Based on the knowledge of what impl Trait
desugars to and how turbofish works, it should be obvious, that impl Trait
in function parameter type annotations disables the turbofish (::<>
) call syntax, and requires the generic parameters to be inferred. There is simply no way to specify the values for implicit generic parameters (denoted in previous paragraphs as __T1
, __T2
, etc.).
trait Trait1 {}
fn blackjack<T>(a: impl Trait1, b: T, c: impl Trait1) { /**/ }
blackjack::<bool, /* now way to pass two params for impl trait π€*/>(/*...*/);
The way how the compiler generates implicit generic parameters for each impl Trait
occurrence is its private implementation detail. It guarantees neither the order nor the position of implicit generic parameters generated from impl Trait
, so we can't explicitly specify the value for these parameters.
The only way for rustc
to know what types for each impl Trait
to use, is via type inference only. This also means we can't specify the value for regular generic parameters other than by letting them be deduced.
For example, it is impossible to call the following function at all.
#![allow(unused)] fn main() { trait Trait1 {} impl Trait for i32 {} fn voldemort<T: Default>(a: impl Trait1) { T::default(); } // No syntax exists to call `voldemort` π£ voldemort::<bool>(99); // (compile error) can't use turbofish voldemort(99); // (compile error) can't infer `T` type parameter }
We are forced to replace all usages of impl Trait
in function parameters with regular generic types.
#![allow(unused)] fn main() { trait Trait1 {} impl Trait1 for i32 {} fn voldemort<T: Default, U: Trait1>(a: U) { T::default(); } // It's callable, yay π! // But now ugly `_` is required on each call π voldemort::<bool, _>(99); }
Even if all remaining generic parameters can be trivially inferred we have to enumerate them all with _
. I recommend never to design such an API that forces users to always write a turbofish with a bunch of _
for generic parameters that can be inferred. Unfortunately, there isn't a better universal workaround for this problem.
There exists an initiative to fix this by letting us use turbofish syntax with impl Trait
parameters being inferred, though I guess it has low priority at the time of this writing π€.
Real World Example
Such a problem occurred for me when writing an extension trait, but I will depict it as a free function here for simplicity. This function maps one collection into the other.
#![allow(unused)] fn main() { fn map_collect<O: FromIterator<T>, I: IntoIterator, T>( iter: I, map: impl FnMut(I::Item) -> T ) -> O { iter.into_iter().map(map).collect() } }
Because this function uses impl Trait
syntax it's impossible to call it with turbofish. For example, we can't instruct rustc
to infer Result<Vec<_>>
for the first type parameter that easily.
#![allow(unused)] fn main() { fn map_collect<O: FromIterator<T>, I: IntoIterator, T>( iter: I, map: impl FnMut(I::Item) -> T ) -> O { iter.into_iter().map(map).collect() } use std::io::Error; // Can't use turbofish to specify that the first type param is `Result<Vec<_>>` map_collect([false, true], |val| Ok::<bool, Error>(val))?; // ^ cannot infer type Ok::<(), Error>(()) }
If we replace impl FnMut(T::Item) -> T
with the fourth generic parameter we will be able to use turbofish for calling the function, but it will be as ugly as this:
#![allow(unused)] fn main() { fn map_collect<O: FromIterator<T>, I: IntoIterator, T, F: FnMut(I::Item) -> T>( iter: I, map: F ) -> O { iter.into_iter().map(map).collect() } use std::io::Error; map_collect::<Result<Vec<_>, Error>, _, _, _>([false, true], |val| Ok(val))?; Ok::<(), Error>(()) }
Conclusions
Now you know what the limitations of impl Trait
are, and how to define a function, that is impossible to call in Rust without uninhabited types.
I hope you learned something new today π.
2022-07-22