Functions and Type Annotations
Every function signature in Rust requires explicit type annotations for all parameters and for any non-() return type. While rustc performs full type inference for local let variables inside a function body, it deliberately never infers parameter types or return types from how a function is implemented or called.
Mandatory Annotations in Function Signatures
fn calculate_discount(price_cents: u64, percent: u8) -> u64 {
─────────── ─── ─────── ── ───
│ │ │ │ └── Explicit return type (required if not `()`)
│ │ └──────┴───────── 2nd parameter and its required type
└────────┴─────────────────────── 1st parameter and its required type
let reduction = price_cents * u64::from(percent) / 100; <-- Local variable `reduction` is inferred as `u64`
price_cents - reduction
}
fn calculate_discount(price_cents: u64, percent: u8) -> u64 {
// Inside the body, `reduction` does not need a type annotation:
let reduction = price_cents * u64::from(percent) / 100;
price_cents - reduction
}
fn main() {
let final_price = calculate_discount(2500, 20);
println!("Final price: {final_price} cents");
}
Final price: 2000 cents
Why Rust Requires Explicit Function Signatures
Some functional languages (such as Haskell and OCaml) use Hindley-Milner global type inference to deduce function parameter and return types without annotations. Rust rejects global function-signature inference in favor of function-local type inference for four engineering reasons:
| Reason | Benefit of Mandatory Signature Types |
|---|---|
| 1. Stable API Contracts | A minor edit inside a function body can never accidentally change the function's parameter or return types and break callers across the codebase |
| 2. Local Reasoning & Documentation | A reader can understand what a function accepts and returns solely by reading its fn header line without reading its implementation |
| 3. Fast, Localized Error Messages | If a function body has a bug, the compiler reports the error inside that function against its declared signature rather than blaming a distant caller |
| 4. Parallel Compilation | The compiler can type-check callers of a function using only its signature without waiting to analyze the function's body |
Caller Function (`main`) Signature Contract Boundary Callee Body (`calculate`)
+-----------------------+ +-----------------------------+ +-------------------------+
| Checked against | ----> | fn calculate(x: i32) -> u64 | <---- | Checked independently |
| signature types only | +-----------------------------+ | against signature types |
+-----------------------+ +-------------------------+
How Function Signatures Drive Type Inference in Callers
Because a function's parameter and return types are fixed and known upfront, calling a function provides strong type constraints that allow rustc to infer the types of variables and generic methods in the caller automatically:
fn accept_port(port: u16) {
println!("Configured port: {port}");
}
fn sum_slice(values: &[i64]) -> i64 {
values.iter().sum()
}
fn main() {
// 1. `"8080".parse()` Normally requires a turbofish or type annotation,
// but passing it to `accept_port(u16)` tells `rustc` that `.parse()` MUST produce `Result<u16, _>`!
let port = "8080".parse().unwrap();
accept_port(port);
// 2. Passing `&numbers` to `sum_slice(&[i64])` infers `numbers` as `Vec<i64>`:
let mut numbers = Vec::new();
numbers.push(100);
numbers.push(200);
let total = sum_slice(&numbers);
println!("Total: {total}");
}
Configured port: 8080
Total: 300
Notice that neither let port = "8080".parse().unwrap(); nor let mut numbers = Vec::new(); needed a local type annotation because the subsequent calls to accept_port(port) and sum_slice(&numbers) uniquely determined their types (u16 and Vec<i64>).
Annotating Destructured Function Parameters
Just like let bindings, a function parameter can destructure a tuple or struct directly in the parameter list. The type annotation always follows the entire parameter pattern after ::
struct Point {
x: i32,
y: i32,
}
fn manhattan_distance((x1, y1): (i32, i32), Point { x: x2, y: y2 }: Point) -> u32 {
x1.abs_diff(x2) + y1.abs_diff(y2)
}
fn main() {
let origin = (0, 0);
let target = Point { x: -4, y: 7 };
println!("Distance: {}", manhattan_distance(origin, target));
}
Distance: 11
Common Compiler Errors
Omitting a Parameter Type
Leaving off a parameter's : Type annotation—or trying to group two parameters under a single type like Go (x, y: i32)—fails to compile:
// ❌ does not compile
fn add(x, y: i32) -> i32 {
x + y
}
fn main() {
println!("{}", add(1, 2));
}
error: expected one of `:` or `@`, found `,`
--> src/main.rs:1:9
|
1 | fn add(x, y: i32) -> i32 {
| ^ expected one of `:` or `@`
|
= note: anonymous parameters are removed in the 2018 edition (see RFC 1685)
help: if this is a parameter name, give it a type
|
1 | fn add(x: TypeName, y: i32) -> i32 {
| ++++++++++
error: aborting due to 1 previous error
Every parameter must have its own : Type (fn add(x: i32, y: i32) -> i32).
Using the Inference Placeholder _ in a Function Signature (E0121)
While let items: Vec<_> = ... is valid inside a function body, the type placeholder _ is forbidden in function parameter and return types:
// ❌ does not compile
fn make_pair(a: i32, b: _) -> (i32, _) {
(a, b)
}
fn main() {}
error[E0121]: the placeholder `_` is not allowed within types on item signatures for functions
--> src/main.rs:1:25
|
1 | fn make_pair(a: i32, b: _) -> (i32, _) {
| ^ ^ not allowed in type signatures
| |
| not allowed in type signatures
|
help: use type parameters instead
|
1 | fn make_pair<T>(a: i32, b: T) -> (i32, T) {
| +++ ~ ~
error: aborting due to 1 previous error
If a function should accept any type, declare an explicit generic type parameter (fn make_pair<T>(a: i32, b: T) -> (i32, T)).