Variables and Mutability

Variables are fundamental to any programming language. In Oxide, every variable binding follows clear rules about mutability that help prevent bugs and make your code easier to reason about. If you're familiar with Rust, you'll find Oxide's approach very similar, but with syntax inspired by languages like Swift and Kotlin.

Immutable Bindings with let

When you declare a variable with let, it creates an immutable binding. This means once you assign a value, you cannot change it:

fn main() {
    let x = 5
    println!("The value of x is: \(x)")
}

If you try to reassign an immutable variable, the compiler will stop you:

fn main() {
    let x = 5
    println!("The value of x is: \(x)")
    x = 6  // Error: cannot assign twice to immutable variable
}

This might seem restrictive at first, but immutability is a powerful feature. When a value cannot change, you can trust that it stays the same throughout your program. This eliminates entire categories of bugs and makes code easier to understand, especially in larger projects.

Rust comparison: The let keyword works identically to Rust. The only visible difference is that semicolons are optional in Oxide.

#![allow(unused)]
fn main() {
// Rust
let x = 5;
println!("The value of x is: {}", x);
}

Mutable Bindings with var

Of course, sometimes you need values that can change. Oxide uses the var keyword for mutable bindings:

fn main() {
    var x = 5
    println!("The value of x is: \(x)")
    x = 6
    println!("The value of x is: \(x)")
}

This outputs:

The value of x is: 5
The value of x is: 6

With var, you can modify the value as many times as needed. Use var when you know a value will change, like a counter in a loop or an accumulator.

Rust comparison: Oxide's var is equivalent to Rust's let mut. The choice of var is intentional, as it's a familiar keyword from Swift, Kotlin, JavaScript, and many other languages.

#![allow(unused)]
fn main() {
// Rust equivalent
let mut x = 5;
x = 6;
}

Constants

Constants are values that are bound to a name and cannot change throughout the entire program. Unlike let bindings, constants:

  • Must always have a type annotation
  • Must be set to a constant expression (evaluated at compile time)
  • Can be declared in any scope, including the global scope
  • Use the const keyword and SCREAMING_SNAKE_CASE by convention
const THREE_HOURS_IN_SECONDS: Int = 60 * 60 * 3

fn main() {
    println!("Three hours is \(THREE_HOURS_IN_SECONDS) seconds")
}

Constants are useful for values that many parts of your code need to know about, like the maximum number of players in a game or the speed of light. Naming hardcoded values as constants helps future readers understand the significance of the value.

Rust comparison: Constants work identically to Rust. The only difference is using Oxide's type aliases (Int instead of i32).

#![allow(unused)]
fn main() {
// Rust
const THREE_HOURS_IN_SECONDS: i32 = 60 * 60 * 3;
}

Shadowing

You can declare a new variable with the same name as a previous variable. This is called shadowing, and the new variable shadows the previous one:

fn main() {
    let x = 5

    let x = x + 1

    {
        let x = x * 2
        println!("The value of x in the inner scope is: \(x)")
    }

    println!("The value of x is: \(x)")
}

This outputs:

The value of x in the inner scope is: 12
The value of x is: 6

Shadowing is different from marking a variable as mutable with var. If we try to reassign without let, we get a compile-time error. By using let, we can perform transformations on a value but have the variable be immutable after those transformations.

Another advantage of shadowing is that you can change the type of a value while reusing the same name:

fn main() {
    let spaces = "   "
    let spaces = spaces.len()
    println!("Number of spaces: \(spaces)")
}

The first spaces is a &str, and the second spaces is a UIntSize (the return type of .len()). This is allowed because we're creating a new variable with let.

If we tried to use var and reassign, we'd get a type mismatch error:

fn main() {
    var spaces = "   "
    spaces = spaces.len()  // Error: mismatched types
}

Type Annotations

Oxide can usually infer the type of a variable from its value, but you can also be explicit:

fn main() {
    let count: Int = 42
    let name: String = "Alice".toString()
    let active: Bool = true

    var score: Float = 0.0
    score = 99.5
}

Type annotations use a colon followed by the type, consistent with languages like TypeScript, Kotlin, and Swift. Oxide provides intuitive type aliases for primitives:

Oxide TypeRust Equivalent
Inti32
Floatf64
Boolbool
UIntSizeusize

We'll explore all the available types in the next section.

Rust comparison: The annotation syntax is the same as Rust, just with Oxide's type names.

#![allow(unused)]
fn main() {
// Rust
let count: i32 = 42;
let active: bool = true;
}

When to Use let vs var

As a general guideline:

  • Default to let: Start with immutable bindings. This makes your code safer and easier to reason about.
  • Use var when needed: If you find you need to modify a value, change it to var.

The compiler will tell you if you've marked something as immutable but try to change it. Following this approach helps you take advantage of the safety that Oxide provides while still having the flexibility to use mutable state when appropriate.

Summary

  • Use let for immutable bindings that cannot change after initialization
  • Use var for mutable bindings that you need to modify
  • Use const for compile-time constants with global scope
  • Shadowing allows reusing names while keeping immutability benefits
  • Type annotations use the format name: Type

Now that you understand how variables work, let's look at the different data types available in Oxide.