That balance is one of Rust’s better design choices. Signatures stay readable, but the code inside them does not turn into annotation noise.
Function syntax#
A basic Rust function looks like this:
fn add(a: i32, b: i32) -> i32 {
a + b
}There are three details worth remembering:
- parameters always have types
- the return type comes after
-> - the final expression becomes the return value if it has no semicolon
Returning values#
The last expression is usually enough:
fn square(n: i32) -> i32 {
n * n
}You can still use return when an early exit is clearer:
fn clamp(x: i32, min: i32, max: i32) -> i32 {
if x < min {
return min;
}
if x > max {
return max;
}
x
}That is common in validation and error handling paths.
Statements versus expressions#
Rust leans heavily on expressions:
let result = {
let base = 10;
base + 5
};The block evaluates to 15. If you add a semicolon to the last line, the block evaluates to ().
This distinction matters because many Rust constructs are really “value-producing blocks.”
Common scalar types#
You do not need to memorize the entire type system to get started. A small set covers most beginner code:
i32,i64: signed integersu32,u64,usize: unsigned integersf32,f64: floating-point valuesbool:trueorfalsechar: a single Unicode scalar value
Examples:
let port: u16 = 8080;
let ready: bool = true;
let initial: char = 'R';
let ratio: f64 = 0.75;Strings: &str versus String#
This is one of the first distinctions every Rust learner hits.
&str is a borrowed string slice:
let language: &str = "Rust";String is an owned, growable string:
let mut name = String::from("Ada");
name.push_str(" Lovelace");As a rule of thumb:
- use
&strfor string data you only need to read - use
Stringwhen you need ownership or mutation
Type inference keeps local code short#
Rust infers types from context whenever it can:
let n = 10;
let name = String::from("m58");You only need an explicit annotation when inference would be ambiguous or when the annotation improves readability:
let ids: Vec<u64> = Vec::new();
let timeout_ms: u64 = 5_000;Beginner code often benefits from a few extra annotations. They are not mandatory, but they can make intent easier to read.
Associated functions and methods#
Not every function is free-standing. Rust also supports associated functions and methods through impl blocks.
struct User {
name: String,
}
impl User {
fn new(name: String) -> Self {
Self { name }
}
fn greeting(&self) -> String {
format!("hello, {}", self.name)
}
}newis an associated function because it does not takeselfgreetingis a method because it takes&self
This is how most Rust types expose behavior.
A realistic example#
fn describe_port(port: u16) -> String {
if port == 443 {
"https".to_string()
} else if port == 80 {
"http".to_string()
} else {
format!("custom:{port}")
}
}This function shows a few common patterns:
- explicit input and output types
- expression-based branching
- returning an owned
String
Summary#
- Rust function signatures are explicit about parameters and return types.
- The last expression usually serves as the return value.
- Type inference keeps most local bindings short.
- The types you will use constantly are numbers,
bool,char,&str, andString.
Once those pieces feel normal, reading Rust APIs gets much easier.