Types · Lesson 6.5

Mutating method

Source
Write methods that change the value they are called on, with self: &var T.

The methods in the last lesson only looked at their rectangle. A method that changes the value it is called on takes self: &var T — the writable reference from Mutable reference. Its writes land in the value before the dot.

This is where methods start to pay off. A Stack is an array plus a count of how much of it is in use, and the two must change together:

struct Stack {
    items: int[4];
    length: uint;
}

If every caller updated items and length by hand, one of them would sooner or later forget the count. With Push and Pop as the only code that changes them, they stay in step.

Reading and writing receivers

The receiver's type says what a method may do:

func IsEmpty(self: &Stack) -> bool {
    return self.length == 0;
}
func Push(self: &var Stack, value: int) -> bool {
    if self.IsFull() {
        return false;
    }
    self.items[self.length] = value;
    self.length += 1;
    return true;
}

IsEmpty takes &Stack and only reads. Push takes &var Stack, so it may assign self.items[…] and self.length. A writing method may also return a value — here, whether the push happened, since a full stack has no room left.

Push calls self.IsFull() on its own receiver: a writing method can always use the reading ones.

How the stack moves

Push writes at position length and then counts the new item; Pop uncounts the top item and then reads it. The last value in is the first one out:

flowchart LR
    e["length 0<br/>`[ _ _ _ _ ]`"] -- "`Push(10)`" --> a["length 1<br/>`[ 10 _ _ _ ]`"]
    a -- "`Push(20)`" --> b["length 2<br/>`[ 10 20 _ _ ]`"]
    b -- "`Pop()` gives `20`" --> c["length 1<br/>`[ 10 _ _ _ ]`"]

Pop does not erase the old value — it stays in the array, but past length it no longer counts.

The caller needs a var

A writing method needs a var to work on, just as assigning a field would:

var stack = Stack { items: [0; 4], length: 0 };
MethodReceiverOn a let stackOn a var stack
IsEmpty, IsFull&Stackallowedallowed
Push, Pop&var Stackrefusedallowed

A rule the caller keeps

Pop has a precondition: it is only to be called on a stack that is not empty. The program keeps it by asking first:

while !stack.IsEmpty() {
    PrintLine("pop  {}", stack.Pop());
}

Making a method report "there was nothing to pop" in its return type is what Optionals are for, a couple of parts on.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// A method that changes the value it is called on takes `self: &var T`, the writable reference
// from the MutableReference lesson. Its writes land in the value before the dot.
//
// This is where methods start to pay off. A `Stack` is an array plus a count of how much of it is
// in use, and the two must change together. If every caller updated them by hand, one would
// sooner or later forget the count. With `Push` and `Pop` as the only code that changes them,
// they stay in step.
import Io::PrintLine;

struct Stack {
    items: int[4];
    length: uint;
}

extend Stack {
    // A reading method still takes `&Stack`, and can be called on any stack.
    func IsEmpty(self: &Stack) -> bool {
        return self.length == 0;
    }

    func IsFull(self: &Stack) -> bool {
        return self.length == 4;
    }

    // A writing method takes `&var Stack`. It may also return a value: here, whether the push
    // happened, since a full stack has no room left.
    func Push(self: &var Stack, value: int) -> bool {
        if self.IsFull() {
            return false;
        }
        self.items[self.length] = value;
        self.length += 1;
        return true;
    }

    // Only to be called on a stack that is not empty; the caller checks `IsEmpty` first.
    func Pop(self: &var Stack) -> int {
        self.length -= 1;
        return self.items[self.length];
    }
}

func Main() -> int {
    // A writing method needs a `var` to work on, just as assigning a field would. Had `stack`
    // been declared with `let`, every `Push` and `Pop` below would be rejected, while `IsEmpty`
    // and `IsFull` would still be allowed.
    var stack = Stack { items: [0; 4], length: 0 };

    for value in 1..=5 {
        let pushed = stack.Push(value * 10);
        PrintLine("push {}  accepted {}", value * 10, pushed);
    }
    PrintLine("full {}", stack.IsFull());

    while !stack.IsEmpty() {
        PrintLine("pop  {}", stack.Pop());
    }
    PrintLine("empty {}", stack.IsEmpty());
    return 0;
}

Run it

cd Examples/Types/MutatingMethod
rux run
push 10  accepted true
push 20  accepted true
push 30  accepted true
push 40  accepted true
push 50  accepted false
full true
pop  40
pop  30
pop  20
pop  10
empty true

Common mistakes

Try it yourself

  1. Add func Peek(self: &Stack) -> int that returns the top item without removing it. Which receiver does it need?
  2. Add func Clear(self: &var Stack) and use it to empty a full stack in one call.
  3. Change var stack to let stack and read which lines the compiler refuses — and which it still accepts.
  4. Add a Size(self: &Stack) -> uint method and print the size after every push.

Learn more