Method patterns: what a struct can become
On this page
You have made a type of your own, a fraction, a sum of money, a matrix, and you want it to work like a built-in one: added with +, compared with ==, indexed with [], walked by each. How does + know how to add two numbers, or [] how to find a list's value? Each is a method with a particular name, and any struct that has a method of that name gains the same ability. Nothing is declared but the method itself. This recipe gathers those patterns: operators, properties, indexing, calling a value like a function, running code as a value is made and as it dies, and being walked by each. The scribes of ancient Egypt wrote fractions as sums of fractions with 1 on top; here is a fraction that adds and compares like a number:
mod rhind;
import stdio use *;
// Euclid's greatest common divisor, from Functions
fn gcd(a i64, b i64) i64 {
if b == 0 { a; } else { gcd(b, a % b); }
}
struct Fraction {
pub num i64;
pub den i64;
// An initializer: new Fraction(6, 8) is made in lowest terms, 3/4
pub fn init(self &new, n i64, d i64) {
let g = gcd(n, d);
*self = new Fraction(num: n / g, den: d / g);
}
// A method named for an operator gives Fraction that operator
pub fn `+`(self, other Fraction) Fraction {
new Fraction(num * other.den + other.num * den, den * other.den);
}
pub fn `==`(self, other Fraction) bool {
num == other.num and den == other.den;
}
pub fn show(self) {
print <- num, "/", den;
}
}
fn main() {
// The Rhind papyrus writes 2/5 as 1/3 + 1/15
let parts = new Fraction(1, 3) + new Fraction(1, 15);
print <- "1/3 + 1/15 = ";
parts.show();
print <- "\n";
if parts == new Fraction(2, 5) {
print <- "The scribe was right\n";
}
if parts != new Fraction(1, 2) {
print <- "and it is not a half\n";
}
}
It prints:
1/3 + 1/15 = 2/5 The scribe was right and it is not a half
Try this: the papyrus writes 2/7 as 1/4 + 1/28. Check it, then give Fraction a `*` method and work out half of two thirds.
Two things here are patterns. The method named `+` is what a + b calls, and `==` is what == calls, bringing != with it. And the method named init runs whenever a Fraction is made, so every fraction comes out in lowest terms, including the one + builds. The rest of the page takes the patterns one at a time.
Operators: a method named for one#
Most operators are method calls in disguise: a + b is a.`+`(b), exactly as it is for numbers. A name made of punctuation is written in backticks. A type that implements the method has the operator; a type that does not is refused with "Method or field `+` not found", which is why a struct cannot be compared until it says how. Any type that encodes numbers or number-like things can use this: fractions, complex numbers, big integers, money in cents, vectors and matrices. The methods are:
- Arithmetic: `+`, `-`, `*`, `/` and `%`, taking the value on the right.
- Logic and shifts: !, &, ^, ~, << and >>, as the number types have them.
- Comparison: `==`, which brings != with it, and `<`, `<=`, `>` and `>=`, each its own method: declaring < gives a type < and nothing more.
A fraction is less than another when crossing the denominators says so:
pub fn `<`(self, other Fraction) bool {
num * other.den < other.num * den;
}
The same == serves a comparison of two borrowed references, since == on references compares the values they refer to: a method declared for references, fn `==`(self &, other &Point) bool, is used as it is, and otherwise the value method is. The identity operators === and !==, which ask whether two references point to the same place, are not methods, and no type overrides them. An enum is given its == rather than declaring one.
- is two operators, taking away and negating, so it shows how two methods share one name. Each has its own name, and declares the shared one with overload between its name and its parameters:
pub fn sub overload `-`(self, other Fraction) Fraction {
new Fraction(num * other.den - other.num * den, den * other.den);
}
pub fn neg overload `-`(self) Fraction {
new Fraction(num: -num, den: den);
}
half - third calls sub, and -half calls neg. A call by the shared name tries every method that overloads it, and exactly one must accept the arguments; none, or more than one, is an error, and calling a method by its own name, half.sub(third), always picks that one. Any method may join a shared name this way, not only an operator: a type might have area() and area(scale). Functions share names the same way.
The operators that change a variable come almost free. a += b uses a `+=` method if the type has one, and otherwise works as a = a + b, so a Fraction with + already has +=; the same holds for the other arithmetic, logic and shift operators. ++x and --x call methods named `++` and `--`, taking self &mut.
Note: Not built yet: the postfix forms, x++ and x--, on a type of your own (their methods are to be named `_++` and `_--`), and a method named `=` that would take over assignment, for a type whose copy must do more than copy its fields.
One more method lets a value stand as a condition. When an if or a while is given something other than a bool, it calls the value's isTrue method, if it has one. A fraction might count as true when it is not zero:
pub fn isTrue(self) bool {
num != 0;
}
if new Fraction(0, 5) {
print <- "never\n";
} else {
print <- "zero is false\n"; // this one runs
}
Computed properties#
A method that takes nothing but self may be called without its parentheses, so it reads like a field:
pub fn value(self) f64 {
f64.from(num) / f64.from(den);
}
print <- third.value, "\n"; // 0.333333
That matters when a type changes. A field made public at first may later need to obey a rule, and become private; a method of the same name, computing the value or checking it, takes the field's place, and code that read x.value reads it still.
Note: Not built yet: a setter, a method that would let x.value = v store through it; that assignment is refused (Error 1016: Expression must be lval).
Being a collection: [] and <-#
A List is a struct, and what makes xs[i] and xs <- v work on it is two methods any struct can have. Here is a sum written down term by term, the way a scribe would list it:
struct Sum {
terms List[Fraction];
pub fn init(self &new) {
*self = new Sum(terms: List[Fraction].empty());
}
// sum <- f adds a term
pub fn add overload `<-`(self &mut, f Fraction) {
terms.push(f);
}
// sum[i] reads term i
pub fn `[]`(self &, i usize) &Fraction {
terms[i];
}
// sum[i] = f, and sum[i].num = n, write through this one
pub fn `&[]`(self &mut, i usize) &mut Fraction {
&mut terms[i];
}
}
var s = new Sum();
s <- new Fraction(1, 4), new Fraction(1, 18), new Fraction(1, 468);
print <- "the second term is 1/", s[1].den, "\n"; // the second term is 1/18
s[2] = new Fraction(1, 500);
The [] method hands back a borrowed reference to the item, which a read goes through; the index may be anything, not only a number, which is how a Dict is indexed by its keys. The &[] method hands back a reference that may be written through, and is used wherever the indexed item is changed: s[i] = v, s[i].num = 3, s[i] += v, a method taking self &mut called on s[i], or &mut s[i]. The place can run on through any chain of fields and indexes, a[g].b[i].n = 5, each index writing through its own type's &[], as long as everything on the way may be written. A swap, s[0] <=> s[1], writes through both sides the same way.
The send operator is the method named `<-`, joined with overload as add does here, so a type can take several kinds of value: print has one for text, one for each kind of number, one for bool. Every entry form of Collections then works: several values, 3 of x, each … yield, and fill x, which also asks the type's len() and capacity() to know when it is full. What is sent need not be kept: a send can serialize a value to a stream, hand it to another thread, or be a directive that changes how the receiver formats what comes next. The receiver is lent to the method as its self says: a self &mut method, as a collection's is, needs a receiver that may be changed, while print's take self & and work on a let global.
Being called: ()#
A value can be called like a function, if its type has a method named `()`. A fraction is a natural share of something, so a Fraction might apply itself to a number:
pub fn `()`(self &, n i64) i64 {
n * num / den;
}
let twoThirds = new Fraction(2, 3);
print <- "two thirds of 90 is ", twoThirds(90), "\n"; // two thirds of 90 is 60
If that looks like a closure, it is one. A closure is a struct with no name and one method, `()`: its state entries are the struct's fields, and its body is the method. So a struct with a `()` method goes wherever a closure goes. A parameter written &<fn(n i64) i64 takes any closure that is called that way, and takes a callable struct just the same:
fn applyTwice(f &<fn(n i64) i64, n i64) i64 {
f(f(n));
}
let k = 3i64;
let triple = fn(n i64) i64 { n * k; };
applyTwice(&twoThirds, 90) // 40
applyTwice(&triple, 5) // 45
A `()` method passed that way takes self &, since it is only lent; one that takes self by value is refused there, with a message that says so. A method taking self &mut is a callable value with state that changes from one call to the next, as a closure with var state is. Closure references has the forms in full.
Making a value: init#
Every struct has an implicit initializer, which takes its fields' values in order or by name. A struct may declare initializers of its own, as Fraction does, when the implicit one would not do: when making a value takes more than storing values, as reducing a fraction does; when a field is private and code outside the module must still make values; or when listing every field is more than callers should have to write. An initializer is a method with these differences from the rest:
- It is named init, and called by new: new Fraction(6, 8) runs it. A type may have several, each with its own name joining the overload name init (fn polar overload init(self &new, r f32, theta f32)), and a construction's arguments select the one that takes them. Where a declared initializer and the implicit one both take values listed in order, the declared one is used; naming the fields always reaches the implicit one, so new Fraction(num: 6, den: 8) is 6/8, unreduced.
- Its self is self &new: memory set aside for a value that does not exist yet. Until it is filled, its fields cannot be read and its methods cannot be called. The new permission is written only here.
- It fills self whole, with another initializer: *self = new Fraction(num: …, den: …), usually the implicit one, reached by naming the fields. Values listed in order that this same initializer takes would select it again, and that is an error. Filling self must happen on every path. After it, the initializer may use the value as any method would.
- It returns nothing: the construction that called it holds what it wrote.
- self does not escape: it is used only through itself, never passed to a function, stored, copied or returned.
- It runs only in a construction, never called by name on a value, which it would overwrite.
A value is constructed once: new Fraction(p), where p is already a Fraction, is an error, since a finished value needs no construction. An enum's variant is constructed without new, as Enums showed.
Ending a value: final#
Some values hold something that must be given back when they are done with: a file to close, a connection to end, a subscription to cancel. A finalizer, the method named final, runs by itself when a value dies, just before its memory is reclaimed. Here a lamp says when it goes off:
struct Lamp {
id i32;
pub fn init(self &new, n i32) {
print <- "lamp ", n, " on\n";
*self = new Lamp(id: n);
}
fn final(self &uni) {
print <- "lamp ", id, " off\n";
}
}
fn main() {
let a = new Lamp(1);
{
let b = new Lamp(2);
print <- "inside the block\n";
}
print <- "end of main\n";
}
It prints:
lamp 1 on lamp 2 on inside the block lamp 2 off end of main lamp 1 off
A type has at most one finalizer. Its self is self &uni, a reference that nothing else shares, and it takes nothing else and returns nothing. It may read the value and call its methods, and should do its work quickly, without waiting on anything. A value dies the same way wherever it lives: a local variable at the end of its block, the newest first, a parameter at the end of its function, a field inside the value holding it. Its own finalizer runs first, then its fields' finalizers, in the order the fields are declared, and so on down; a tuple's elements and an array's die in order too.
A type with a finalizer is moved rather than copied (Moves, the recipe before this one). And a method named clone is an ordinary method: no copy calls it, and declaring one does not make a type copyable. Exactly when a value dies in every case, including a value made in the middle of an expression, a value moved out on only some paths, and a value in a region, is in When a value dies, in Memory.
Being walked: next#
each walks anything with a method named next that answers an Option: Some with the next item, or None when there are no more. Such a value is called a cursor. Fibonacci, the same one whose sequence fills the Functions page, also worked out how to write any fraction as Egyptian unit fractions: take the largest one that fits, and repeat with what is left. As a cursor:
// Hands out the unit fractions that add up to num/den, largest first:
// each next() gives one more denominator, or None when nothing is left
struct Egyptian is Iterator[i64] {
num i64;
den i64;
pub fn next(self &mut) Option[i64] {
if num == 0 { return None; }
let d = (den + num - 1) / num; // the largest unit fraction 1/d that fits
num = num * d - den; // what is left: num/den - 1/d
den = den * d;
Some(d);
}
}
print <- "4/13 =";
each d in new Egyptian(4, 13) {
print <- " 1/", d; // 4/13 = 1/4 1/18 1/468
}
The cursor keeps its place in its own fields, and each calls next until it answers None. A cursor in a variable is advanced there, so a loop left early with break can be followed by another that carries on from where it stopped.
A cursor needs nothing but next for each to walk it. The is Iterator[i64] after the struct's name does one thing more: it declares that this cursor hands out i64s, and with that it gains every step from Collections: map, filter, count, sum and the rest. Each of those is an adapter, a method that takes a cursor and hands back another cursor, one that changes each item on its way through (map) or lets only some through (filter), doing no work until something walks it:
new Egyptian(4, 13).count() // 3 new Egyptian(4, 13).filter(d => *d < 100).count() // 2: 1/4 and 1/18 new Egyptian(3, 7).map(d => d * 2).sum() // 490: 3/7 is 1/3 + 1/11 + 1/231
Without the declaration, each still walks the cursor, but asking it for map is an error that says to declare it. That is the one place in this page where a declaration is needed: the adapters are written once, in the core library's Iterator trait, and a type declares the trait to have them. The adapters lists every one, and Traits explains them.
A collection is not a cursor itself, since walking it should not use it up. It has a method named iter that hands out a fresh cursor, and each calls it, so each x in xs means each x in xs.iter(). The scribe's Sum can do the same by lending its list's cursor:
pub fn iter(self &) ListIter[Fraction] {
terms.iter();
}
each f in s {
print <- " 1/", f.den; // 1/4 1/18 1/468
}
You can now give a struct of your own the operators of a number, the indexing and sending of a collection, the calling of a function, a say in how its values begin and end, and the ability to be walked. That is how List, Dict and the rest are built, and nothing about them is reserved for the library. One of those abilities brings a restriction with it: a type with a finalizer can no longer be copied, only moved, and the next recipe shows how to work with a value there can only be one of.