On this page
Compound data types and functions
Compound data types
Rust has two built-in compound data types: tuples and arrays. Both have a fixed size known at compile time and live on the stack.
Tuples
A tuple groups values of different types. Its size and types are part of the tuple's type signature:
fn main() {
// Tuple with three different types
let person: (String, u8, f64) = (
String::from("Ana Garcia"),
30,
1.75,
);
// Access by index with a dot
println!("Name: {}", person.0);
println!("Age: {}", person.1);
println!("Height: {:.2}m", person.2);
// Destructuring
let (name, age, height) = person;
println!("{name} is {age} years old and {height:.2}m tall");
// Empty tuple: () is called "unit" — the implicit return type of void functions
let nothing: () = ();
// Single-element tuple (note the comma)
let singleton = (42,);
println!("Singleton: {}", singleton.0);
}Tuples are especially useful for returning multiple values from a function without creating a struct.
Arrays
Arrays in Rust have a fixed length known at compile time. All elements must be the same type:
fn main() {
// Explicit declaration: [type; length]
let days: [&str; 7] = [
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"
];
// Initialize all elements with the same value
let zeros: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0]
let ones = [1_u8; 10]; // [1, 1, ..., 1] x10
// Index access — Rust verifies bounds at runtime
println!("First day: {}", days[0]);
println!("Last day: {}", days[6]);
// Length
println!("Days in a week: {}", days.len());
// Iteration
for day in &days {
println!("{day}");
}
// Slices: references to a portion of the array
let weekend: &[&str] = &days[5..7];
println!("Weekend: {:?}", weekend);
}The difference between arrays and vectors (Vec<T>) is that arrays have a fixed size. Vectors can grow and are stored on the heap. For most dynamic collections you will use Vec<T>.
Functions in Rust
Functions are declared with the fn keyword. Unlike some languages, all parameters require explicit type annotations — Rust does not infer types of parameters in order to keep function signatures always clear and self-documenting.
// Function without parameters or return (implicitly returns ())
fn greet() {
println!("Hello from a function!");
}
// Function with parameters and return type
fn square(n: i32) -> i32 {
n * n // No semicolon: return expression
}
// Multiple parameters
fn format_temperature(celsius: f64, unit: &str) -> String {
match unit {
"F" => format!("{:.1}°F", celsius * 1.8 + 32.0),
"K" => format!("{:.1}K", celsius + 273.15),
_ => format!("{:.1}°C", celsius),
}
}
fn main() {
greet();
let n = 5;
println!("{n}² = {}", square(n));
println!("{}", format_temperature(100.0, "F"));
println!("{}", format_temperature(0.0, "K"));
}Expressions vs statements
This distinction is fundamental in Rust and different from many other languages:
- A statement performs an action but does not produce a value. It ends with
; - An expression evaluates to a value. It does not end with
;
fn main() {
// Statement: let is a statement
let x = 5;
// Expression: the { } block is an expression
let y = {
let temp = x * 2;
temp + 1 // No ;: this is the block's value
};
println!("y = {y}"); // y = 11
// if is also an expression in Rust
let parity = if x % 2 == 0 { "even" } else { "odd" };
println!("x is {parity}");
// Compare with:
let _z = {
let temp = x * 2;
temp + 1; // With ;: the block produces ()
};
// _z is of type ()
}This feature makes Rust very expressive. You can use if, blocks, match, and almost any language construct as part of an expression.
Early return with `return`
Use return when you need to exit a function before reaching the end:
fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
return f64::NAN; // Early return
}
// Normal return at the end
a / b
}
fn classify_number(n: i32) -> &'static str {
if n < 0 {
return "negative";
}
if n == 0 {
return "zero";
}
"positive" // Implicit return at end
}
fn main() {
println!("{}", divide(10.0, 3.0));
println!("{}", divide(10.0, 0.0));
for n in [-5, 0, 7] {
println!("{n} is {}", classify_number(n));
}
}Functions returning multiple values
Rust doesn't have native multiple returns, but tuples emulate them perfectly:
fn statistics(data: &[f64]) -> (f64, f64, f64) {
let n = data.len() as f64;
let sum: f64 = data.iter().sum();
let mean = sum / n;
let min = data.iter().cloned().fold(f64::INFINITY, f64::min);
let max = data.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
(mean, min, max)
}
fn main() {
let measurements = [23.5, 18.2, 31.7, 25.0, 19.8, 27.3];
let (mean, min, max) = statistics(&measurements);
println!("Mean: {mean:.2}");
println!("Min: {min:.2}");
println!("Max: {max:.2}");
println!("Range: {:.2}", max - min);
}The `!` type (never)
Rust has a special type ! called "never" that represents functions that never return:
fn panic_function() -> ! {
panic!("Something went very wrong!");
}
fn infinite_loop() -> ! {
loop {
// This never terminates
}
}
fn main() {
// The ! type is compatible with any expected type
let x: i32 = if true { 42 } else { panic!("impossible") };
println!("{x}");
}The macros panic!, todo!, unimplemented!, and unreachable! all have type !.
With these foundations about compound types and functions, you are ready for the most important concept in Rust: ownership, the system that makes memory safety without a garbage collector possible.
// Functions are declared with fn
// Parameters ALWAYS require type annotations
fn add(a: i32, b: i32) -> i32 {
a + b // No semicolon: return expression
}
fn calculate_area(base: f64, height: f64) -> f64 {
let area = base * height / 2.0;
area // implicit return
}
// Return multiple values with a tuple
fn min_max(list: &[i32]) -> (i32, i32) {
let min = *list.iter().min().unwrap();
let max = *list.iter().max().unwrap();
(min, max)
}
fn main() {
let result = add(3, 7);
println!("3 + 7 = {result}");
let area = calculate_area(5.0, 4.0);
println!("Area = {area:.2}");
let nums = [3, 1, 4, 1, 5, 9, 2, 6];
let (min, max) = min_max(&nums);
println!("Min = {min}, Max = {max}");
}
Sign in to track your progress