Skip to content

← Generics and Traits step 17 of 24

Medium Primitives

Named generics and impl Trait parameters

Fix the call to apit in the starter while keeping all three function signatures unchanged. For each input integer, return its debug representation, a |, and the debug representation of its decimal string. Preserve input order and duplicates; an empty input produces an empty result.

For 1, the output string is:

1|"1"

This contains five characters. The string's debug representation, "1", contributes three of them: a quote, a digit, and a quote.

Both functions are generic

These definitions accept values whose types implement Debug:

use std::fmt::Debug;

pub fn generic<T: Debug>(t: T) -> String {
    format!("{t:?}")
}

pub fn apit(t: impl Debug) -> String {
    format!("{t:?}")
}

fn main() {
    assert_eq!(generic::<i64>(1), "1");
    assert_eq!(generic(1_i64), "1");
    assert_eq!(apit(1_i64), "1");
    assert_eq!(apit(String::from("1")), "\"1\"");
}

generic names its type parameter T. The other form, argument-position impl Trait (APIT), introduces an anonymous generic type parameter. It is still generic; it does not turn the argument into a trait object.

The named form allows generic::<i64>(...), where ::<...> is commonly called a turbofish. APIT's anonymous parameter cannot be supplied this way. That is why the starter's apit::<String>(...) call fails with E0107.

Remove the explicit type argument from that call. Keep generic::<i64>(v) to demonstrate the distinction.

You can still choose the argument's type

APIT does not prevent callers from controlling which concrete type they pass. Annotate the value or use a constructor with an explicit type:

use std::fmt::Debug;

fn show(value: impl Debug) -> String {
    format!("{value:?}")
}

fn main() {
    let value: i64 = 7;
    assert_eq!(show(value), "7");
    assert_eq!(show(Vec::<i64>::new()), "[]");
}

The restriction concerns explicit generic arguments on show itself, not type annotations elsewhere in the expression.

Repeated type parameters express a relationship

Two separate APIT parameters can have different types. Reusing one named parameter requires both arguments to fit the same type:

use std::fmt::Debug;

fn independent(a: impl Debug, b: impl Debug) -> String {
    format!("{a:?}|{b:?}")
}

fn same<T: Debug>(a: T, b: T) -> String {
    format!("{a:?}|{b:?}")
}

fn main() {
    assert_eq!(independent(1_i64, "one"), "1|\"one\"");
    assert_eq!(same(1_i64, 2_i64), "1|2");
    // same(1_i64, "one") would fail: these types do not match.
}

Use a named type parameter when you need to refer to that type elsewhere in the signature or in a where clause. APIT is useful when the bound on an individual argument says everything you need. Either form can appear in a public API.

Changing between the forms can break callers that specify generic arguments. For example, replacing generic<T: Debug> with an anonymous parameter makes existing generic::<i64> calls invalid. Review call-site compatibility before making that change in a library.

Why the output contains quotes

Both helpers use Debug formatting, written {:?} or {value:?}. For an integer, that gives its decimal representation. For a string, it includes quotes and escapes where needed. Display, written {} or {value}, prints the string's contents without those surrounding quotes.

In run, the named helper receives the integer and the anonymous helper receives v.to_string(). The different output comes from those argument types, not from which generic syntax the functions use.

The optional clippy::impl_trait_in_params restriction lint discourages APIT in public signatures. It is allowed by default; -D warnings alone does not enable it. Follow a project's explicitly selected lint policy rather than treating that preference as a language rule.

Further reading: anonymous type parameters in the Rust Reference and the Clippy lint.