Viscosity
Review
Part IX · Rust for TypeScript developers · Day 32

127. Structs and impl

The particle has been an object since chapter 3. In Rust it becomes a struct: a record with a fixed shape, its methods kept in a separate block, and every method saying out loud how it uses the particle.

About 16 minutes, including the exercises and quiz
By the end you will be able to
  • Define a struct, build a value of it and read and change its fields.
  • Attach methods and associated functions to a type with an impl block.
  • Choose between &self, &mut self and self for each method.
  • Explain how Rust lays an array of structs out in memory.

A particle with a fixed shape

Here is the particle from chapter 3, in TypeScript:

interface Particle {
  x: number; y: number;
  vx: number; vy: number;
}
const p: Particle =
  { x: 0, y: 0, vx: 3, vy: 4 };

The interface is only a promise to the type checker. At run time p is an ordinary object, and the engine has to discover its shape for itself (the shapes of chapter 63). Rust has no ordinary objects. You declare the shape first, and every value is built from it.

Intuition

A printed form with labelled boxes. Every copy of the form has the same boxes in the same places, and you can’t hand one in with a box missing or with an extra box scribbled in the margin.

Structs

struct Particle {
    x: f64,
    y: f64,
    vx: f64,
    vy: f64,
}

let mut p = Particle {
    x: 0.0, y: 0.0, vx: 3.0, vy: 4.0,
};
p.x += p.vx;

You must give every field, and their types are checked. Fields are read and written with a dot, as in TypeScript. Mutability belongs to the binding: let mut p makes all of p changeable, and you can’t mark just one field as mutable[1].

When a variable has the same name as a field, you can write it once. Particle { x, y, vx: 0.0, vy: 0.0 } means x: x, y: y. That is field init shorthand.

A struct moves like a String does. let q = p; moves the particle into q, and p can’t be used afterwards (chapter 124). Chapter 131 shows how to make small structs copy instead.

Definition

A struct is a named record type: a fixed list of fields, each with a name and a type. A struct expression Name { field: value, ... } builds a value and must give every field. value.field reads or writes one field, and writing needs a mut binding.

Methods live in impl

A TypeScript class (chapter 7) puts fields and methods in one block. Rust splits them. The struct holds the data, and an impl block attaches functions to the type:

impl Particle {
    fn new(vx: f64, vy: f64) -> Self {
        Particle { x: 0.0, y: 0.0, vx, vy }
    }

    fn speed(&self) -> f64 {
        let v2 = self.vx * self.vx
            + self.vy * self.vy;
        v2.sqrt()
    }

    fn step(&mut self, dt: f64) {
        self.x += self.vx * dt;
        self.y += self.vy * dt;
    }
}

new has no self. It is an associated function, called on the type: Particle::new(3.0, 4.0). There is no new keyword in Rust, and new is just the usual name for a function that builds a value. Inside the impl, Self is another name for Particle.

speed and step are methods: their first parameter is self, the value the method was called on. You call them with a dot, p.speed().

Definition

An impl block attaches functions to a type. Those whose first parameter is self are methods, called as value.method(). The rest are associated functions, called as Type::function(). In the block, Self names the type being implemented.

Three kinds of self

JavaScript’s this is whatever object you called the method on, and the method can do anything to it. Rust’s self is a parameter like any other, so its type says exactly what the method may do.

Step through&self, &mut self and self
let mut p = Particle::new(3.0, 4.0);
let s = p.speed();
p.step(1.0);
p.step(1.0);
let q = p.stopped();
p.speed(); // ✗ p was moved
A Particle struct with fields x, y, vx and vy, and an arrow from the method being called to the structCALLERMETHODp: Particlex0y0vx3vy4Particle::new(vx, vy)
new has no self at all. It is an associated function, called on the type with ::, and it builds a Particle at the origin. Like a constructor, but just an ordinary function.
1/6
  • &self borrows the value to read it. Most methods are like this.
  • &mut self borrows it to change it. The caller needs let mut.
  • self takes ownership. The value moves into the method, and the caller can’t use it afterwards. It is rare, and used when a method turns the value into something else[1].

You don’t write the borrow at the call. p.step(1.0) means Particle::step(&mut p, 1.0). Rust looks at the receiver type and adds the & or &mut for you[1]. That makes the borrowing rules from chapter 125 apply to every method call. Forget the mut (real rustc 1.97 output, trimmed):

error[E0596]: cannot borrow `p` as
  mutable, as it is not declared as
  mutable

Use p after p.stopped() and the error even explains why:

note: `Particle::stopped` takes
  ownership of the receiver `self`,
  which moves `p`
Definition

The self receiver is a method’s first parameter. &self (short for self: &Self) borrows the value shared, &mut self borrows it mutably, and self takes ownership of it. Method calls borrow automatically to match.

Try itA particle with methods
struct Particle {
    x: f64,
    y: f64,
    vx: f64,
    vy: f64,
}

impl Particle {
    fn new(vx: f64, vy: f64) -> Self {
        Particle {
            x: 0.0,
            y: 0.0,
            vx,
            vy,
        }
    }

    fn speed(&self) -> f64 {
        let v2 = self.vx * self.vx
            + self.vy * self.vy;
        v2.sqrt()
    }

    fn step(&mut self, dt: f64) {
        self.x += self.vx * dt;
        self.y += self.vy * dt;
    }

    fn stopped(mut self) -> Self {
        self.vx = 0.0;
        self.vy = 0.0;
        self
    }
}

fn main() {
    let mut p = Particle::new(3.0, 4.0);
    println!("speed {}", p.speed());
    p.step(1.0);
    p.step(1.0);
    println!("at {} {}", p.x, p.y);
    let q = p.stopped();
    println!("speed {}", q.speed());
}
Open in Rust Playground ↗
Under the hood

The Rust Drip VM is mostly structs with methods. Its Vm struct owns the value stack, and the receivers say who changes what (from companion/rust/src/vm.rs):

fn push(&mut self, value: Value)
fn pop(&mut self) -> Value
fn peek(&self, distance: usize)
    -> Value

Struct layout is where Viscosity comes back in. The default Rust layout promises very little: fields don’t overlap, and each one starts at an offset its alignment allows. The compiler is free to reorder them. What it does promise is that an array of T puts element n exactly n * size_of::<T>() bytes from the start[2]. So [Particle; 1000] is an array of structs in the most literal sense: the fields sit inline, one particle after another, with no pointer to chase. A JavaScript array of objects holds references to objects that live elsewhere, which is what made pointer chasing slow in chapter 20. Chapter 139 measures these layouts, and chapter 140 turns one into a struct of arrays by hand.

Your turn

ExerciseThe VM’s stack as a struct

Finish this fixed-size value stack, adapted from push, pop and peek in the Rust Drip VM. top is the number of values on the stack, so the top value is at slots[top - 1]. peek(0) is the top value, peek(1) the one under it (d is the distance down from the top), and peeking removes nothing. The receivers are already written: notice which methods need &mut self. Replace each todo!(), which stops the program if it is ever reached.

struct Stack {
    slots: [f64; 8],
    top: usize,
}

impl Stack {
    fn new() -> Self {
        Stack {
            slots: [0.0; 8],
            top: 0,
        }
    }

    fn push(&mut self, v: f64) {
        todo!()
    }

    fn pop(&mut self) -> f64 {
        todo!()
    }

    fn peek(&self, d: usize) -> f64 {
        todo!()
    }

    fn len(&self) -> usize {
        self.top
    }
}
Open in Rust Playground ↗
Extra practiceSource spans

A Span marks a byte range of source code, like a token’s position. Write len(&self), the number of bytes it covers, and join(self, b), which consumes both spans and returns the smallest span covering both. a.min(b) and a.max(b) work on usize.

struct Span {
    start: usize,
    end: usize,
}

impl Span {
    fn len(&self) -> usize {
        0
    }

    fn join(self, b: Span) -> Span {
        self
    }
}
Open in Rust Playground ↗

What you learned

  • A struct is a named record with typed fields. Build it with every field, read with a dot, and change it through a mut binding.
  • An impl block attaches functions to a type. Methods take self; associated functions like new don’t, and are called with Type::.
  • The self receiver says how a method uses the value: &self reads, &mut self changes, self consumes. Calls borrow automatically.
  • An array of structs stores the structs inline, back to back.

Next: enums and match, Rust’s own discriminated unions, and the match over opcodes at the heart of the Rust VM.

Check your understanding

0/6
  1. 1.

    Does this compile?

    let p = Particle::new(3.0, 4.0);
    p.step(1.0); // step(&mut self, ..)
  2. 2.

    A method only reads two fields and returns a number. Which receiver should it take?

  3. 3.

    let q = p.stopped(); where stopped takes self. Which is true afterwards?

  4. 4.

    How do you call fn new(vx: f64, vy: f64) -> Self from impl Particle?

  5. 5.

    What does the default Rust layout promise about a struct’s fields?

  6. 6.

    In TypeScript, a class bundles fields and methods together. How does Rust split them?

References

  1. [1] Steve Klabnik, Carol Nichols, Chris Krycho (2026). The Rust Programming Language (3rd edition). No Starch Press; free online at doc.rust-lang.org/book. doc.rust-lang.org/book/ (checked 2026-10-31)
  2. [2] The Rust Reference contributors. Type layout. The Rust Reference. doc.rust-lang.org/reference/type-layout.html (checked 2026-10-31)