Skip to content

← Generics and Traits step 2 of 24

Easy Primitives

Generic functions and monomorphisation

A generic function is written once and compiled many times. When you write

fn tn<T>() -> &'static str {
    std::any::type_name::<T>()
}

T is a type parameter: a placeholder the caller fills in. Rust does not keep T around at runtime. Instead, for every distinct T your program actually uses, the compiler stamps out a separate specialised copy of the function with T replaced by the concrete type. That process is called monomorphisation — literally “making it one-shaped”.

This is what people mean by Rust’s “zero-cost abstraction” claim. Generic code costs nothing at runtime because by the time the machine sees it, there is no generic code left. The costs are real but they are paid elsewhere: bigger binaries and longer compiles, because five instantiations means five copies of the machine code.

In this exercise you make monomorphisation directly observable. std::any::type_name::<T>() reports the fully-qualified name of whatever T the compiler chose for that copy. Different callers, different answers, one source function.

Your task

pub fn describe_instantiations(kinds: Vec<String>) -> Vec<String>

For each input string, call tn instantiated at the matching type and return the name it reports. The names are not what you would type in source — they are the compiler’s canonical paths:

you ask for you get
u8 u8
i64 i64
bool bool
String alloc::string::String
Vec<i64> alloc::vec::Vec<i64>
Vec<String> alloc::vec::Vec<alloc::string::String>
Option<u8> core::option::Option<u8>
&str &str
(u8, bool) (u8, bool)

Anything else returns the literal "unknown".

Read that table again: String really does live in the alloc crate and Option really does live in core. std mostly re-exports them. Seeing the true paths once makes the standard library’s structure legible.

The turbofish

The starter contains "u8" => tn(), and it does not compile:

error[E0282]: type annotations needed

Nothing in tn() mentions T. Not the arguments (there are none), not the return type (&'static str regardless). The compiler has no way to guess, and it refuses to guess. The fix is to say so explicitly with the turbofish:

tn::<u8>()

::<...> is how you supply type arguments in expression position. It looks odd because the plain < would be ambiguous with the less-than operator, so the language requires the :: first. You will also see it as "42".parse::<i32>() and collect::<Vec<_>>().

Two neighbouring errors worth knowing:

  • Supply the wrong number of type arguments — tn::<u8, bool>() — and you get E0107, “function takes 0 generic arguments but 2 were supplied” (well, 1 in this case). E0107 is the arity error for generics.
  • Assign the result to the wrong type and you get the familiar E0308.

A gate rule you should know about now

Under clippy -D warnings a type parameter that is never used is an error:

error: type parameter `T` goes unused in function definition
       [clippy::extra_unused_type_parameters]
help: consider removing the parameter

A parameter counts as used if it appears in an argument type, in the return type, in the body, or behind a PhantomData. tn<T> passes because type_name::<T>() genuinely uses it; fn dead<T>(x: u8) { ... } does not.

One carve-out is worth knowing because it looks like an inconsistency. On clippy 0.1.95 the lint skips functions with an empty body, which keeps the classic static-assertion idiom legal:

fn assert_send<T: Send>() {}         // accepted — empty body
fn assert_send<T: Send>(_x: u8) {}   // also accepted — still empty
fn nearly<T: Send>(_x: u8) { let _ = 1; }   // REJECTED — body is not empty

Add one statement to that third function and the lint fires. If you meet it, the question to ask is “does this parameter do any work?”, not “did I annoy the linter”.

Remember the grade is compile + tests + clippy -D warnings.