Rust - Syntax & Basics

Overview

Estimated time: 30–45 minutes

Understand Rust's fundamental syntax, the distinction between expressions and statements, and basic language conventions that make Rust unique among systems programming languages.

Learning Objectives

Prerequisites

Expressions vs Statements

One of Rust's key concepts is the distinction between expressions and statements:

Statements

Statements perform actions but don't return values:


fn main() {
    let x = 5;        // Statement: variable binding
    let y = 10;       // Statement: another binding
    
    println!("Hello"); // Statement: macro call
}

Expressions

Expressions evaluate to values and can be used anywhere a value is expected:


fn main() {
    let x = 5;
    let y = {
        let inner = 3;
        inner + 1      // Expression: no semicolon!
    };                 // y = 4
    
    println!("y is: {}", y);
    
    // If is an expression
    let condition = true;
    let number = if condition { 5 } else { 6 };
    println!("number is: {}", number);
}

Output:

y is: 4
number is: 5

Basic Syntax Elements

Code Blocks

Blocks create new scopes and are expressions:


fn main() {
    let x = 5;
    
    let y = {
        let x = 3;      // Shadows outer x
        x + 1           // Returns 4
    };                  // x goes back to 5
    
    println!("x: {}, y: {}", x, y);
}

Output:

x: 5, y: 4

Semicolons Matter

Adding a semicolon turns an expression into a statement:


fn returns_value() -> i32 {
    42              // Expression: returns 42
}

fn returns_unit() -> () {
    42;             // Statement: returns () (unit type)
}

fn main() {
    let x = returns_value();
    let y = returns_unit();
    
    println!("x: {}, y: {:?}", x, y);
}

Output:

x: 42, y: ()

Function Syntax


// Function with parameters and return type
fn add_numbers(a: i32, b: i32) -> i32 {
    a + b           // Expression return
}

// Function with explicit return
fn multiply(a: i32, b: i32) -> i32 {
    return a * b;   // Statement return
}

// Function with no return value (returns unit type)
fn print_sum(a: i32, b: i32) {
    println!("Sum: {}", a + b);
}

fn main() {
    let sum = add_numbers(5, 3);
    let product = multiply(4, 6);
    
    print_sum(sum, product);
    
    println!("Sum: {}, Product: {}", sum, product);
}

Output:

Sum: 29
Sum: 8, Product: 24

Variable Bindings and Patterns

Basic Bindings


fn main() {
    let x = 5;              // Immutable binding
    let mut y = 10;         // Mutable binding
    
    y = 15;                 // OK: y is mutable
    // x = 10;              // Error: x is immutable
    
    println!("x: {}, y: {}", x, y);
}

Destructuring


fn main() {
    let tuple = (1, 2, 3);
    let (a, b, c) = tuple;  // Destructure tuple
    
    let array = [1, 2, 3, 4, 5];
    let [first, second, ..] = array;  // Destructure array
    
    println!("a: {}, b: {}, c: {}", a, b, c);
    println!("first: {}, second: {}", first, second);
}

Output:

a: 1, b: 2, c: 3
first: 1, second: 2

Comments and Documentation

Regular Comments


fn main() {
    // Single line comment
    let x = 5;  // Comment at end of line
    
    /*
     * Multi-line comment
     * Can span multiple lines
     */
    let y = 10;
}

Documentation Comments


/// This is a documentation comment for the function below
/// It supports **markdown** formatting
/// 
/// # Examples
/// 
/// ```
/// let result = add_one(5);
/// assert_eq!(result, 6);
/// ```
fn add_one(x: i32) -> i32 {
    x + 1
}

fn main() {
    let result = add_one(5);
    println!("Result: {}", result);
}

Naming Conventions

Standard Conventions


// Variables and functions: snake_case
let my_variable = 42;
fn my_function() {}

// Types: PascalCase
struct MyStruct {}
enum MyEnum {}

// Constants: SCREAMING_SNAKE_CASE
const MAX_SIZE: usize = 100;
static GLOBAL_VAR: i32 = 42;

// Modules: snake_case
mod my_module {}

Macros vs Functions


fn main() {
    // Function call
    println("Hello");       // Error: not a function
    
    // Macro call (note the !)
    println!("Hello");      // OK: macro
    
    // Other common macros
    vec![1, 2, 3];          // vec! macro
    format!("Hello {}", "world");  // format! macro
    panic!("Something went wrong"); // panic! macro
}

Control Flow Syntax


fn main() {
    let number = 6;
    
    // If expression
    let description = if number % 2 == 0 {
        "even"
    } else {
        "odd"
    };
    
    // Match expression
    let size = match number {
        1..=5 => "small",
        6..=10 => "medium",
        _ => "large",
    };
    
    println!("Number {} is {} and {}", number, description, size);
}

Output:

Number 6 is even and medium

Common Pitfalls

Semicolon Confusion


fn wrong() -> i32 {
    let x = 5;
    x + 1;          // Error: returns (), not i32
}

fn correct() -> i32 {
    let x = 5;
    x + 1           // OK: returns i32
}

Shadowing vs Mutation


fn main() {
    let x = 5;
    let x = x + 1;      // Shadowing: creates new variable
    
    let mut y = 5;
    y = y + 1;          // Mutation: changes existing variable
    
    println!("x: {}, y: {}", x, y);
}

Checks for Understanding

Question 1

What's the difference between these two code blocks?


let x = {
    let y = 3;
    y + 1
};

let x = {
    let y = 3;
    y + 1;
};
Click to see answer

The first block returns the value 4 (expression), while the second returns () (statement due to semicolon). The first x will be 4, the second won't compile if expecting an integer.

Question 2

Will this function compile? Why or why not?


fn mystery() -> i32 {
    if true {
        42
    } else {
        "hello"
    }
}
Click to see answer

No, it won't compile. Both branches of an if expression must return the same type. One returns i32, the other returns &str.

Question 3

What will this print?


fn main() {
    let x = 5;
    let x = "hello";
    let x = x.len();
    println!("{}", x);
}
Click to see answer

It will print "5". This demonstrates variable shadowing - each let creates a new variable that shadows the previous one.


← PreviousNext →