Types · Lesson 6.4

Method

Source
Give a type its own functions with extend, and call them on a value as value.Method().
You'll need: Struct, Reference

So far a function that works on a struct stands on its own and is handed the struct: Area(card). A method is a function that belongs to a type and is called on a value of it: card.Area(). The value comes first, so the reader sees at once what is being asked about — and the type's behaviour is gathered in one place instead of scattered among free functions.

The extend block

Methods are declared in an extend block, apart from the struct. The struct stays a plain list of fields; the extend block holds what the type can do:

struct Rectangle {
    width: float64;
    height: float64;
}

extend Rectangle {
    func Area(self: &Rectangle) -> float64 {
        return self.width * self.height;
    }
}

The self receiver

The value a method works on is its first parameter, called the receiver. It is always named self, and its type is written out like any other parameter's. self: &Rectangle is the read-only reference from the Reference lesson, so these methods may read the rectangle but not change it.

Inside a method, fields are always reached through self. self is an ordinary parameter, not an implied scope — a bare width would be an unknown name.

Calling a method

The value before the dot becomes self, borrowed just as an argument would be. The remaining arguments go in the parentheses:

PrintLine("card in envelope {}", card.FitsInside(envelope));
flowchart LR
    call["card.FitsInside(envelope)"] --> self["self = card<br/>(borrowed as &Rectangle)"]
    call --> other["other = envelope"]
    self --> body["body of Rectangle's FitsInside"]
    other --> body

That call matches the declaration parameter for parameter — self first, then other:

func FitsInside(self: &Rectangle, other: &Rectangle) -> bool {
    return self.width <= other.width && self.height <= other.height;
}

Same name, different types

Each type has its own methods. Circle can have an Area of its own without clashing with the one on Rectangle:

extend Circle {
    func Area(self: &Circle) -> float64 {
        return 3.14159 * self.radius * self.radius;
    }
}

Which one runs is decided by the value before the dot: card.Area() runs the rectangle's, coin.Area() the circle's. Two free functions named Area would have to be told apart by their parameter types, as overloading does; methods simply live with their type.

Free functionMethod
func Area(card: &Rectangle)func Area(self: &Rectangle) in extend
Called as Area(card)Called as card.Area()
Lives among all the package's functionsLives with its type

The program

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

Src/Main.rux
// So far a function that works on a struct stands on its own and is handed the struct:
// `Area(card)`. A method is a function that belongs to a type and is called on a value of it:
// `card.Area()`. The value comes first, and the reader sees at once what is being asked about.
//
// Methods are declared in an `extend` block, apart from the struct, which stays a plain list of
// fields. The value a method works on is its first parameter, which is always named `self` and
// has its type written out like any other parameter. `self: &Rectangle` is the read-only
// reference from the Reference lesson, so these methods may read the rectangle but not change it.
import Io::PrintLine;

struct Rectangle {
    width: float64;
    height: float64;
}

extend Rectangle {
    // Inside a method, fields are always reached through `self`. A bare `width` would be an
    // unknown name: `self` is an ordinary parameter, not an implied scope.
    func Area(self: &Rectangle) -> float64 {
        return self.width * self.height;
    }

    func IsSquare(self: &Rectangle) -> bool {
        return self.width == self.height;
    }

    // Further parameters come after `self`.
    func FitsInside(self: &Rectangle, other: &Rectangle) -> bool {
        return self.width <= other.width && self.height <= other.height;
    }
}

struct Circle {
    radius: float64;
}

// Each type has its own methods, so `Circle` can have an `Area` of its own without clashing with
// the one on `Rectangle`. Which one runs is decided by the value before the dot.
extend Circle {
    func Area(self: &Circle) -> float64 {
        return 3.14159 * self.radius * self.radius;
    }
}

func Main() -> int {
    let card = Rectangle { width: 9.0, height: 6.0 };
    let envelope = Rectangle { width: 11.0, height: 11.0 };
    let coin = Circle { radius: 1.25 };

    // The value before the dot becomes `self`, borrowed just as an argument would be.
    PrintLine("card area        {}", card.Area());
    PrintLine("envelope area    {}", envelope.Area());
    PrintLine("coin area        {:.2}", coin.Area());
    PrintLine("card square      {}", card.IsSquare());
    PrintLine("envelope square  {}", envelope.IsSquare());

    // The remaining arguments go in the parentheses, as for any call.
    PrintLine("card in envelope {}", card.FitsInside(envelope));
    PrintLine("envelope in card {}", envelope.FitsInside(card));
    return 0;
}

Run it

cd Examples/Types/Method
rux run
card area        54.0
envelope area    121.0
coin area        4.91
card square      false
envelope square  true
card in envelope true
envelope in card false

Common mistakes

Reaching a field without self.
return width * self.height; fails with error: name 'width' is not defined in this scope. Write self.width: fields are never in scope by themselves.
Naming the receiver something else.
A first parameter called this is not a receiver, just an ordinary parameter. With func Area(this: &Rectangle), the call card.Area() fails with error: call to 'Area' expects 1 argument, but 0 were provided. The receiver is always self.
Calling a method like a free function.
Area(card) fails with error: name 'Area' is not defined in this scope. A method belongs to its type, so it is reached through a value: card.Area().
Leaving out the parentheses.
card.Area without () is read as a field, and fails with error: struct 'Rectangle' has no field 'Area'. A method is called, even when it takes no further arguments.

Try it yourself

  1. Add func Perimeter(self: &Rectangle) -> float64 and print the card's perimeter.
  2. Give Circle an IsLargerThan(self: &Circle, other: &Circle) -> bool method.
  3. Add func Scaled(self: &Rectangle, factor: float64) -> Rectangle that returns a new rectangle, and print the area of card.Scaled(2.0).
  4. Remove self. from one field in Area and read the error.

Learn more