impl Trait in Rust explanation

Igor Polyakov
3 min readMay 23, 2018

--

In Rust 1.26 a new feature called impl Trait was stabilized. How does it work? Instead of specifying an exact type, you can say that your function either returns or takes something that implements a trait.

When returning closures, you have no other way to actually specify the type of the closure, since each closure’s type cannot be otherwise named (each closure is unique). So the new syntax allows you to do something like this:

fn apply<A, B, C, F, G>(mut f: F, a: A) 
-> impl FnMut(&B) -> C
where F: FnMut(B) -> G,
G: FnMut(A) -> C,
B: Copy,
A: Clone {
move |b| f(*b)(a.clone())
}

It’s a function that accepts a closure and returns an unboxed closure. This syntax is the only way to do it, and it wasn’t possible to do this without boxing the closure prior to this.

But you can even use the same feature to simplify this grotesque signature.

fn apply<A, B, C, G>(mut f: impl FnMut(B) -> G, a: A)
-> impl FnMut(&B) -> C
where
G: FnMut(A) -> C,
B: Copy,
A: Clone {
move |b| f(*b)(a.clone())
}

Here, we noticed that F was just plumbing. It was only an explicit parameter to the function for the purpose of naming the argument which was also a closure. It wasn’t referenced by other parts of the function, so we could make this substitution. These two functions do exactly the same thing, they’re both statically dispatched.

But we can’t do this:

fn apply<A, B, C>(mut f: impl FnMut(B) -> impl FnMut(A) -> C, a: A)
-> impl FnMut(&B) -> C
where
B: Copy,
A: Clone {

move |b| f(*b)(a.clone())
}

We get this error

`impl Trait` not allowed outside of function and inherent method return types

This is because we attempted to put impl Trait into our trait and so far that’s not allowed. So in the argument case, we can only do it once, we can’t actually nest them inside of each other.

Another function I have seems like it could benefit from impl Trait actually doesn’t.

fn accumulate<'a, T>(tuples: &[(&'a str, &Fn(i32) -> bool)], i: i32) -> T
where T: Monoid + From<&'a str> + From<String> {
...
}

If I try to rewrite it as

fn accumulate<'a>(tuples: &[(&'a str, &Fn(i32) -> bool)], i: i32) -> impl Monoid + From<&'a str> + From<String> {

...
}

it won’t compile and give me an error message. Why is that? We did the same transformation on the argument in the previous case, but in the return case it didn’t work.

This is because of what impl T means in the return. The error message at the call site tells us:

expected struct `std::string::String`, found anonymized type

In our apply function, the caller expected us to generate a type that conformed to FnMut trait. In other words, the callee chose the exact type by generating a closure inside the function.

In our accumulate function, the caller chooses the type. I have two functions that call accumulate and they specify which type it outputs.

pub fn fizzbuzz<'a>(tuples: &[(&'a str, &Fn(i32) -> bool)], i: i32) -> String {
accumulate(tuples, i)
}

pub fn cowbuzz<'a>(tuples: &[(&'a str, &Fn(i32) -> bool)], i: i32) -> Cow<'a, str> {
accumulate(tuples, i)
}

Because the caller chooses the type of the output, impl Trait cannot be used in the return position. But when the caller chooses the type of the input, you can use impl Trait then. To explain this better, I can show an alternative sugar that would have worked for my accumulate function:

fn accumulate<'a>(tuples: &[(&'a str, &Fn(i32) -> bool)], i: i32) -> any Monoid + From<&'a str> + From<String> {

...
}

Rust doesn’t have this kind of sugar, but this should be a good way to remember it. When you expect to return some type (caller doesn’t know what exact type), you can use impl Trait . When you expect to return any type (caller specifies exact type), you can’t use impl Trait. For arguments, you can use impl Trait when the caller specifies the type.

Let me know if this makes the distinction clearer. The full repository can be found at https://bitbucket.org/iopq/fizzbuzz-in-rust/src

--

--