Structs: named fields and methods
On this page
A tuple keeps values together, but only by position: was it the width first, or the height? A struct is a tuple whose values have names, plus everything names make possible: a type of your own, values with defaults, parts kept private, and methods, the things a value of the type can do. This page shows how to declare a struct, build its values, give it methods, and work on one value at a time. Here are points and rectangles:
mod shapes;
import stdio use *;
// A Point is a pair of numbers whose values have names
struct Point {
pub x f32 = 0.; // a field may have a default value
pub y f32 = 0.;
// A function of the type, with no 'self': called as Point.origin()
pub fn origin() Point {
new Point(); // both fields take their defaults
}
// A method: 'x' and 'y' here are self.x and self.y
pub fn dist(self, other Point) f32 {
let dx = other.x - x;
let dy = other.y - y;
(dx * dx + dy * dy).sqrt;
}
// A method that gives back a new Point, and leaves this one as it was
pub fn scaled(self, k f32) Point {
new Point(x * k, y * k);
}
// A method that changes the Point it is called on
pub fn moveBy(self &mut, dx f32, dy f32) {
x += dx;
y += dy;
}
pub fn show(self) {
print <- "(", x, ", ", y, ")";
}
}
// A struct may hold another struct as a field
struct Rect {
pub corner Point;
pub w f32 = 1.;
pub h f32 = 1.;
pub fn area(self) f32 {
w * h;
}
pub fn center(self) Point {
new Point(corner.x + w / 2., corner.y + h / 2.);
}
// Two values handed back at once, as a tuple
pub fn size(self) f32, f32 {
w, h;
}
}
fn main() {
let a = new Point(x: 3., y: 4.); // fields named...
let b = new Point(1., 2.); // ...or listed in order
print <- "a is "; a.show(); print <- ", ", a.dist(Point.origin()), " from the origin\n";
print <- "a doubled is "; a.scaled(2.).show(); print <- "\n";
var box = new Rect(corner: b, w: 4., h: 2.);
print <- "box area ", box.area(), ", center "; box.center().show(); print <- "\n";
box.corner.moveBy(1., 1.); // box is a var, so its corner may move
print <- "moved, its center is "; box.center().show(); print <- "\n";
let w f32;
let h f32;
w, h = box.size();
print <- "box is ", w, " by ", h, "\n";
}
It prints:
a is (3, 4), 5 from the origin a doubled is (6, 8) box area 8, center (3, 3) moved, its center is (4, 4) box is 4 by 2
Try this: give Rect a perimeter method, or a grow method that changes its width and height, and call it on box.
Fields: values with names#
A struct gives a group of values a name, and the name is a new type, used like any other: for a variable, a parameter, a return value, or a field of another struct, as Rect holds a Point. Each field is declared like a variable, with a name and a type, and the struct's values keep their fields together in memory. Names are what a tuple lacked: box.w says what pair.0 never could.
A value is built with new, the struct's name, and the fields' values in parentheses. They may be named, new Point(x: 3., y: 4.), or listed in the order the fields are declared, new Point(1., 2.). A field with a default value may be left out, so new Point() is the origin, and with nothing to give, the empty parentheses may go too: new Point. Every field without a default must be given a value; leave one out and the compiler says "Not enough values specified on type literal". A field's default must be a constant: a literal, a named constant, a constant expression worked out from them, a variant of an enum that holds no value, or a tuple or an array of such constants.
A field is read with a dot, a.x, and changed the same way, p.y = p.x, if the variable holding the value is var. In a let variable every field is as fixed as the variable: "You do not have permission to modify lval". A field of a field works the same way, which is how box.corner moves above.
A struct may hold any type as a field except itself, since a value that contains itself would be infinitely large, and the compiler says so: "Bad contains Bad by value: infinite size. Break the cycle by holding it through a reference." A struct may hold a List of itself, though, since a list keeps its items in a block of their own, and that is how a tree is built:
struct Node {
pub id i64;
pub kids List[Node];
}
A struct may also have no fields at all, struct Nothing {}. Its values take no room, yet they can be passed around and can have methods, which makes an empty struct a useful marker. Cone's own void is one.
Methods: what a value can do#
A method is a function written inside the struct whose first parameter is named self: the value the method is called on. a.dist(b) calls dist with a as its self and b as other. Inside a method, a field or another method of the same type can be named on its own, so x is short for self.x and clear() for self.clear(), as long as no local variable of the method has the same name. Inside the struct, Self names the struct itself, and a parameter's type may be left off when it is Self: fn dist(self, other) f32 means the same as the version above.
A method's name belongs to its type, so Point's dist and some other type's dist never get in each other's way, nor does either clash with a variable or function outside. Within one type, though, a field and a method may not share a name: "Duplicate name n: every function/method needs its own name."
Most methods in the program take self as a value and give back an answer, or a new value. scaled builds a new Point and leaves a as it was, and that makes a program easy to follow: a is the same after every such call as before it. Since each gives back a value, the next method can be called straight on it, a.scaled(2.).show(), a chain that reads left to right.
A function inside the struct with no self belongs to the type rather than to any one value. It is called through the type's name: Point.origin(). It is the natural home for ways of making a value that need a name of their own.
Methods belong to a type that has a declaration to write them in: a struct, an enum, or a trait. A tuple or an array has none, so a group of values that needs methods is made a struct, with the tuple or the array as a field.
Methods that change their value#
moveBy is different: its self is written self &mut, and that says the method may change the value it is called on. x += dx changes this point's own x, not a copy of it. Such a method can only be called on a value that may be changed: one in a var variable, or a field of one, as box.corner.moveBy(1., 1.) is. The variable is lent to the method for the length of the call. On a let variable the call is refused:
let p = new Point(1., 2.); p.moveBy(1., 1.); // Error 1034: Cannot borrow a mutable reference to `p`, which is not mutable
The & marks a borrowed reference: the method is handed a way to reach the caller's value, rather than a copy of it. self &, with no mut, reaches the value only to read it, which spares copying a large one. The Memory part explains borrowed references in full; for a method, it is enough to read self &mut as "may change the value it is called on", and to give the type's methods that change nothing a plain self.
Working on one value: with blocks#
A chain suits methods that each hand back a value. When several statements in a row work on the same value, a with block names it once. Inside the block the value is this, and a bare dot reaches its fields and methods:
let box = new Rect(corner: Point.origin(), w: 4.);
with box {
print <- .w, " by ", .h, ", area ", .area(), "\n"; // 4 by 1, area 4
}
.w is box.w, and .area() is box.area(). In this form this is the value given, to read and to call methods on; a block that changes the value works through a borrowed reference to it. with blocks, in Recipes, has that form and the rest of the operators that act on this.
Copying a struct#
Like a number, a struct value is copied when it is assigned, passed to a function or handed back from one. A change to the copy leaves the original as it was:
var p = new Point(3., 4.); var q = p; q.x = 0.; // p.x is still 3
That is usually what you want for a small value such as a point. For a large value, or when a function should change the caller's value rather than its own copy, the function takes a borrowed reference instead, as a method's self &mut does. A struct with a field that is not copied, such as a String or a list, is moved instead of copied, as that field would be (Moves, in Recipes, has the rule in full).
Two struct values cannot be compared with == until the struct says what makes two of its values equal: "Method or field `==` not found." Only the type knows whether two points that differ in the tenth decimal place are the same point. How a struct gains ==, and + and the rest, is on Method patterns, in Recipes.
Public and private#
The pub in front of each field and method makes it public: code anywhere may use it. Without pub it is private to the module that declares the struct: any code in that module may use it, through any value of the struct, and no code outside may. That is how a type keeps its promises. When a field's value must obey some rule, a fraction's denominator never 0, a list's count never more than its room, the field is left private, and the type's own methods, which keep the rule, are the only way to change it. A struct whose fields must be kept even from the rest of its module is declared in a module of its own.
Privacy reaches construction too. Code outside the module cannot give a private field a value with new, though it may leave a private field with a default to its default. And a public field of a public struct may not have a type that is private to the module, since code outside would then be holding a type it cannot name.
You can now gather values into a type of your own, give it methods that answer questions or change it, and decide what the rest of the program may touch. Given methods with particular names, a struct can become a good deal more: added with +, indexed with [], called like a function, walked by each, and set to run code as each value is made and as it dies. Method patterns, in Recipes, gathers those, once the collections have shown them at work. Next, a type whose values come in kinds: enums.