Aiming for Jarvis, Creating D.A.N.I.

Tutorial 5: Structs and methods (or, How to give your data an identity)

By now, you've got variables, loops, and functions. You're probably starting to notice that your code is becoming a giant collection of loosely associated bits. It’s like having a workbench where the screws, wires, and coffee mugs are all jumbled together in a pile. Structs are the cardboard boxes that keep your desk—and your brain—sane.

Defining a struct: Grouping the chaos

A struct is just a custom type that lets you bundle related data together. If you’re building something complex, you don't just want x and y coordinates floating around in the ether. You want them tied to a Point.

struct Point {
    x: int
    y: int
}

Every field gets its own line, keeping everything tidy. It’s about being explicit; I’ve spent too many nights tracking down bugs that were just a misplaced x in a global sea of variables.

Creating instances

Using your new struct is straightforward.

fn main() -> int {
    p := Point{x: 3, y: 4}
    print("p = ({p.x}, {p.y})")
    return 0
}

It’s satisfying to see your own custom data types actually work. It feels less like "programming" and more like "building."

Methods: Giving your data some brains

Data by itself is just storage. Methods are how you give that data a job to do. By attaching a function to a struct, you turn a passive bucket of values into something that knows how to behave.

struct Rectangle {
    width: int
    height: int

    pub fn area(self: Rectangle) -> int {
        return self.width * self.height
    }

    pub fn perimeter(self: Rectangle) -> int {
        return 2 * (self.width + self.height)
    }
}

fn main() -> int {
    r := Rectangle{width: 4, height: 5}
    print("Area: {r.area()}")
    print("Perimeter: {r.perimeter()}")
    return 0
}

Note that pub keyword—it’s how we mark methods as "public" so other parts of your project can talk to them. It keeps the internal mess private while letting the world interact with your clean interface.

Pointer receivers: Actually getting your hands dirty

Sometimes, you need to change the state of your object. If your Counter doesn't actually count, it’s just a paperweight. When a method needs to modify the struct, we use a pointer receiver (*Counter).

struct Counter {
    value: int

    pub fn increment(self: *Counter) {
        self.value = self.value + 1
    }
}

fn main() -> int {
    c := Counter{value: 0}
    c.increment()
    print("Count: {c.value}")
    return 0
}

Think of it as giving the function a direct line to the memory address rather than just a photocopy of the data. It's dangerous if you're careless, but necessary if you want to actually get things done.

Exercises

Don't let the theory stop you—put these into practice before you lose the momentum:

  1. The Scaler: Add a scale method to the Rectangle struct that multiplies width and height by a factor.
  2. Circle Geometry: Create a Circle struct with a radius field and an area method. Don't forget that isn't going to implement itself. You can hardcode Pi for now; we won't judge.
  3. The Bank Account: Implement a BankAccount struct with deposit and balance methods. See if you can write a withdraw method that checks if you have enough funds first.

Next

When you're ready to stop putting all your code in one massive file that takes ten minutes to scroll through, we'll talk about Modules and imports.