← Effective AI Code Development

Rust Series Part 1: Getting Started with Rust — Immutability and Type Safety

Understanding Rust's safety model through simple examples and a runnable Fibonacci program.

·12 min read·
#Rust#Programming Languages#Type Safety#Memory Safety#Immutability#Ownership#Getting Started#AI Engineering#AI-SDLC

TL;DR

Rust is a systems programming language designed to help developers write reliable, memory-safe, and efficient software.

What makes Rust interesting is how it catches many programming mistakes during compilation, before the application runs.

Its safety model combines several language features:

  • Type safety: Ensures that operations respect the types of their values.
  • Immutability by default: Prevents unintended changes to variable bindings unless mutability is explicitly allowed.
  • Ownership and borrowing: Control how values are owned, shared, and accessed.
  • Compile-time checks: Prevent many memory-safety errors and data races in safe Rust code without requiring a garbage collector.

These features work together, but each addresses a different category of programming problems.

In this first article of the Rust series, we'll understand these concepts, install Rust, and write a simple Fibonacci program to explore its syntax and behavior.

Introduction

When learning a new programming language, one of the things I look at is how it helps developers avoid common mistakes.

Many programming errors come from unintended state changes, incorrect memory access, or assumptions about how data is shared between functions and threads.

Languages such as C, C++, Java, Python, and JavaScript provide different ways to manage these problems. Some rely on developer discipline, some provide runtime checks, and others use garbage collection to manage memory.

Rust takes a different approach.

It uses a combination of a strong static type system, ownership, borrowing, and immutability by default to catch many mistakes during compilation.

For example, in Rust, variables declared using let are immutable by default. If we want to change a variable's value, we must explicitly declare it as mutable using mut.

This may appear to be a small language-design decision, but it encourages developers to be deliberate about state changes.

Beyond immutability, Rust's ownership and borrowing rules help the compiler prevent problems such as dangling references, use-after-free errors, and data races in safe code.

Rust provides these guarantees without requiring a garbage collector.

This makes it worth exploring for systems programming, infrastructure tooling, command-line applications, embedded software, and performance-sensitive services.

But before exploring advanced Rust concepts, let's understand a few fundamentals.

1. Understanding Type Safety in Rust

Type safety means that a programming language enforces rules about which operations are valid for particular types.

Consider this example:

fn main() {
    let count: u32 = 10;

    println!("Count: {}", count);
}

Here:

  • let declares a variable binding.
  • count is the variable name.
  • u32 represents an unsigned 32-bit integer.
  • 10 is the assigned value.

Now, what happens if we try to assign a string to the same variable?

fn main() {
    let mut count: u32 = 10;

    count = "hello";
}

The Rust compiler rejects this program because "hello" is a string slice (&str), not a u32.

Notice that we declared count as mutable.

Mutability does not remove type safety.

Rust allows us to change a mutable variable's value, but the new value must still satisfy its declared type.

This is an important distinction.

Type safety determines whether operations are valid for a type. Mutability determines whether a particular value or binding can be modified.

2. Why Rust Uses Immutability by Default

In many programming languages, developers can reassign variables unless they explicitly restrict modification.

Rust reverses this default for local variable bindings.

Consider:

fn main() {
    let number = 10;

    number = 20;

    println!("{}", number);
}

This program will not compile.

Rust reports an error because number was declared as an immutable binding.

To allow reassignment, we must explicitly use mut:

fn main() {
    let mut number = 10;

    number = 20;

    println!("{}", number);
}

Output:

20

Both immutable and mutable bindings are valid Rust constructs.

The difference is that Rust requires us to make the intention to modify a binding explicit.

Why does this matter?

Consider an application where a value is calculated, passed through multiple functions, and used to make a decision.

If the value changes unexpectedly, debugging becomes difficult.

Immutability helps reduce these unintended changes by making reassignment a compile-time error.

However, immutability by default does not mean that all data in Rust is permanently immutable.

Rust supports mutable references, variable shadowing, and interior mutability when needed.

The important principle is that Rust makes modification explicit and controls how mutable access occurs.

A note about variable shadowing

Rust also allows a new variable binding to reuse an existing name.

fn main() {
    let number = 10;

    let number = number + 5;

    println!("{}", number);
}

Output:

15

This is called shadowing.

We haven't reassigned the original immutable binding. Instead, we've created a new binding with the same name.

Shadowing is different from mutability, and we'll explore it further in later articles.

3. Ownership and Borrowing: The Foundation of Rust's Memory Safety

Immutability is useful, but Rust's memory-safety guarantees depend heavily on its ownership and borrowing model.

In languages that provide direct memory management, developers must carefully manage allocation, access, and deallocation.

Incorrect memory handling can introduce issues such as:

  • Accessing memory after it has been freed.
  • Using dangling pointers.
  • Freeing the same allocation multiple times.
  • Accessing shared mutable data concurrently without proper synchronization.

Rust addresses many of these problems through compiler-enforced ownership and borrowing rules.

What is ownership?

In Rust, each value has an owner, and ownership determines who is responsible for that value.

Ownership can move from one variable to another.

For example:

fn main() {
    let message = String::from("Hello, Rust!");

    let another_message = message;

    println!("{}", another_message);
}

This program works.

However, if we try to access message after the ownership transfer:

println!("{}", message);

The compiler rejects it because ownership of the String has moved to another_message.

This helps prevent accessing a value through an owner that is no longer valid.

Not every assignment transfers ownership. Types such as u32 implement the Copy trait, so their values can be copied instead.

What is borrowing?

Sometimes we want to access a value without transferring ownership.

Rust allows this through references.

fn main() {
    let message = String::from("Hello, Rust!");

    let borrowed = &message;

    println!("{}", borrowed);
    println!("{}", message);
}

Both statements work.

The &message expression creates a shared reference to the original value.

The reference allows us to read the value without taking ownership.

Rust also enforces rules around mutable and shared references.

For example:

fn main() {
    let mut message = String::from("Hello");

    let borrowed = &message;

    message.push_str(", Rust!");

    println!("{}", borrowed);
}

This code fails to compile.

Why?

Because borrowed is a shared reference that remains in use, while push_str() requires mutable access to the same value.

Rust prevents these conflicting accesses.

This is one of the ways its borrowing rules help prevent memory-safety errors.

There is more to ownership and borrowing than these examples, including lifetimes, reference counting, and interior mutability. We'll explore them in detail in the next article.

4. Does Rust Prevent Every Programming Bug?

No.

Rust's type system and ownership model provide strong safety guarantees, but they do not make software automatically secure or correct.

For example, Rust does not automatically prevent:

  • Incorrect business logic.
  • Deadlocks between threads.
  • All logical race conditions.
  • Integer overflow.
  • Resource exhaustion.
  • Vulnerabilities introduced through unsafe code or external dependencies.

Safe Rust prevents undefined behavior caused by data races, but logical concurrency problems can still occur.

Similarly, Rust does not require garbage collection, but this does not mean every Rust operation has zero runtime overhead.

Operations such as bounds checking, synchronization, reference counting, and memory allocation can still introduce runtime costs.

The advantage is that Rust enforces many memory-safety rules at compile time without requiring runtime ownership tracking.

From a software engineering perspective, this is useful because we can catch certain classes of defects earlier in the development lifecycle.

But we still need testing, code reviews, dependency management, security checks, and good application design.

5. Let's Write Our First Rust Program

Now that we've covered the basic concepts, let's write a simple program.

We'll implement a function that calculates a Fibonacci number.

The Fibonacci sequence begins with:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55...

Each number after the first two is calculated by adding the previous two numbers.

Mathematically:

F(0) = 0
F(1) = 1

F(n) = F(n-1) + F(n-2)

We'll use recursion to implement this logic.

This example will help us understand:

  • Functions and parameters.
  • Rust's integer types.
  • Pattern matching using match.
  • Immutable variables.
  • Recursive function calls.
  • Printing output using macros.

The complete program

fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

fn main() {
    let n = 10;

    println!(
        "The {}th Fibonacci number is: {}",
        n,
        fibonacci(n)
    );
}

Let's run this program first and then understand how it works.

6. Installing Rust and Running the Program

Rust provides a toolchain installer called rustup and a package manager and build tool called Cargo.

Step 1: Install Rust

Visit the official Rust installation website:

https://rustup.rs/

Follow the installation instructions for your operating system.

Once installed, verify the tools:

rustc --version
cargo --version

rustc is the Rust compiler.

cargo helps create projects, manage dependencies, compile applications, and run tests.

Step 2: Create a Rust project

Open your terminal and execute:

cargo new fibonacci
cd fibonacci

Cargo creates a project containing files such as:

fibonacci/
├── Cargo.toml
└── src/
    └── main.rs

Cargo.toml contains package configuration and dependency information.

src/main.rs contains the entry point for our executable.

Step 3: Add the Fibonacci code

Open src/main.rs and replace its contents with the Fibonacci program.

Step 4: Run the program

Execute:

cargo run

Cargo compiles the program and runs the generated executable.

You should see:

The 10th Fibonacci number is: 55

We've now created and executed our first Rust application.

7. Understanding the Fibonacci Program

Let's walk through the code and understand how Rust's language features are used.

Function declaration

fn fibonacci(n: u32) -> u32 {

The fn keyword declares a function.

Here:

  • fibonacci is the function name.
  • n is the input parameter.
  • u32 specifies that the parameter is an unsigned 32-bit integer.
  • -> u32 indicates that the function returns a u32.

Using an unsigned integer makes sense for this example because Fibonacci indices are non-negative.

However, it doesn't automatically prevent arithmetic overflow when Fibonacci values become large.

By default, function parameters are immutable bindings in Rust.

We cannot reassign n inside the function unless we explicitly declare the parameter mutable.

Pattern matching with match

match n {
    0 => 0,
    1 => 1,
    _ => fibonacci(n - 1) + fibonacci(n - 2),
}

Rust's match expression compares a value against different patterns.

In this example:

  • When n is 0, return 0.
  • When n is 1, return 1.
  • For all other values, calculate the sum of the previous two Fibonacci numbers.

The _ pattern matches any remaining value.

An important feature of Rust's pattern matching is that the compiler checks whether the patterns cover all possible values.

This is called exhaustive pattern matching.

Unlike some traditional switch statements, Rust's match does not implicitly fall through from one arm to another.

Recursive function calls

fibonacci(n - 1) + fibonacci(n - 2)

This expression calls the same function twice with smaller input values.

Notice that n itself is not modified.

Expressions such as n - 1 and n - 2 calculate new values.

This demonstrates how we can perform computations using immutable bindings.

The main function

fn main() {
    let n = 10;

For a conventional Rust executable, main() is the program's entry point.

The statement:

let n = 10;

creates an immutable variable binding.

Rust infers its type from how the value is used. In this program, n is inferred as u32 because it is passed to fibonacci(), which expects a u32.

If we attempt to reassign n, the compiler will reject the operation.

We don't need mutability here because the input value remains unchanged.

Printing the result

println!(
    "The {}th Fibonacci number is: {}",
    n,
    fibonacci(n)
);

println! is a Rust macro.

Macros use the ! syntax and can expand into Rust code during compilation.

In this example, {} represents a formatting placeholder.

The first placeholder receives n, while the second receives the result of fibonacci(n).

Rust checks the format string and its arguments during compilation, helping catch formatting mistakes early.

8. Where Is Immutability Used in This Program?

Let's summarize the important parts.

CodeBehavior
let n = 10;Creates an immutable binding
n: u32Declares an immutable function parameter by default
match nEvaluates the value without modifying it
fibonacci(n - 1)Calculates a new value rather than changing n
fibonacci(n - 2)Calculates another value without modifying n

Our Fibonacci function does not need mutable variables.

This doesn't mean mutable variables are bad or should always be avoided.

For many algorithms, mutable state is useful and can make an implementation simpler or more efficient.

Rust encourages developers to choose mutability deliberately.

9. Is Our Fibonacci Implementation Production-Ready?

Not yet.

The recursive Fibonacci implementation is useful for learning, but it has two important limitations.

Repeated calculations

Consider:

fibonacci(5)

The function calculates:

fibonacci(4) + fibonacci(3)

But fibonacci(4) also calculates fibonacci(3).

This means the same values are calculated repeatedly.

The naive recursive implementation has exponential time complexity, approximately O(φⁿ), where φ is the golden ratio.

For larger inputs, it becomes inefficient.

Integer overflow

Our function returns u32.

The maximum value of u32 is:

4,294,967,295

The 48th Fibonacci number is:

4,807,526,976

That exceeds the maximum value supported by u32.

In a default debug build, arithmetic overflow triggers a panic.

In a typical release build with overflow checks disabled, unsigned arithmetic wraps around. Overflow-check behavior can be configured, so we shouldn't assume that every release build behaves identically.

For applications that need to handle larger inputs reliably, we should consider:

  • A more efficient iterative algorithm.
  • Checked arithmetic using checked_add().
  • A larger integer type or arbitrary-precision integers when required.
  • Appropriate input validation and error handling.

We'll revisit these concepts in a future article.

For now, the recursive implementation serves its purpose: understanding Rust's syntax, functions, immutability, and pattern matching.

10. What I've Learned from This First Rust Program

Even a small program introduces several useful aspects of Rust.

1. Rust is explicit about types.

Function parameters and return types help the compiler validate operations.

2. Immutability is the default.

Variables cannot be reassigned unless we explicitly allow it.

3. Pattern matching is expressive.

The match expression provides a structured way to handle different cases, with compiler-enforced exhaustiveness.

4. Ownership and borrowing go beyond immutability.

They provide rules for managing how values are owned and accessed, helping prevent memory-safety errors in safe Rust.

5. Compile-time safety does not replace engineering discipline.

We still need to think about algorithm efficiency, arithmetic overflow, testing, security, and maintainability.

For me, this is one of the interesting aspects of Rust.

The language encourages developers to think carefully about how data is represented, accessed, and modified.

These decisions become especially relevant when building infrastructure utilities, security tools, and systems software where reliability and predictable behavior matter.

References


Public profile lookup

Ask AI About the Author

Open this query in ChatGPT, Claude, or Perplexity.

Comments

Comments are open to confirmed email subscribers. Use the email you subscribed with. To edit a comment, delete it and post a new one.

0/2000
Verify:

    Subscribe to get the new blogs.

    Field notes from someone who ships before they write about it. Sovereign AI, AI-SDLC, DevOps, and what 59 production deployments teach you. No spam. Unsubscribe anytime.

    Related field notes