Functions

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)).