On this page

Compound data types and functions

14 min read TextCh. 1 — Rust Fundamentals

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.

Expressions vs statements
In Rust almost everything is an expression that produces a value. A block { ... } is an expression whose value is the last line without a semicolon. Adding ; converts an expression into a statement that produces ().
Do not mix implicit and explicit return
Use return only for early returns. The last line of a function body without ; is the normal return. Mixing them at the same position is redundant and non-idiomatic in Rust.
rust
// 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}");
}