Types · Lesson 6.7

Extension

Source
Add methods to a type you did not write, from int to the slice int[..].
You'll need: Method, Slice

extend is not only for types you declared yourself. It can add methods to a type you did not write — the built-in ones included — and from then on, in this package, every value of that type has them. 42.IsEven() reads better than IsEven(42), and a slice that can Sum() itself saves passing it to a helper every time.

Extending a built-in type

The block looks exactly like the ones in Method, with a built-in type's name after extend:

extend int {
    func IsEven(self: int) -> bool {
        return self % 2 == 0;
    }

    // Clamped follows in the same block.
}

From here on, any int can be asked: answer.IsEven(), volume.Clamped(0, 100).

Receivers by value

These receivers are taken by value — self: int, not self: &int. A number or a slice is small, and copying it is as cheap as borrowing it. The three receiver forms now on the table:

ReceiverThe method getsTypical for
self: Ta copysmall values: numbers, slices, enum cases
self: &Tthe caller's value, to readstructs and arrays that only need reading
self: &var Tthe caller's value, to changemethods that update the value

A slice is a view, so even taken by value it still sees the caller's elements — copying the slice copies the view, not the numbers behind it.

Extending a slice

Any type can be extended, compound types included:

extend int[..] {
    func Sum(self: int[..]) -> int {
        var total = 0;
        for value in self {
            total += value;
        }
        return total;
    }

    // CountEven follows in the same block.
}

Inside CountEven, the IsEven added a few lines earlier is used like any other method: value.IsEven(). Extensions build on one another.

One exact type

The extended type must be one exact type, and methods do not spread to its relatives:

flowchart LR
    ext["extend int[..]"] --> yes["int[..]<br/>has Sum and CountEven"]
    ext -. "not" .-> no1["uint8[..]"]
    ext -. "not" .-> no2["int32[..]"]
    ext -. "not" .-> no3["int[5]<br/>(an array, not a slice)"]

int32 is not int, uint8[..] is not int[..], and an array is not a slice. Each would need an extend of its own. An array reaches the slice methods through a view of itself:

PrintLine("sum of the last 3: {}", scores[2..].Sum());

The same view, scores[..] or the let all: int[..] = scores; in the program, gives the whole array.

The program

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

Src/Main.rux
// `extend` is not only for types you declared yourself. It can add methods to a type you did not
// write, the built-in ones included, and from then on, in this package, every value of that type
// has them.
//
// The receiver here is taken by value: `self: int`, not `self: &int`. A number or a slice is small,
// and copying it is as cheap as borrowing it. A slice is a view, so even by value it still sees
// the caller's elements.
import Io::PrintLine;

extend int {
    func IsEven(self: int) -> bool {
        return self % 2 == 0;
    }

    func Clamped(self: int, low: int, high: int) -> int {
        if self < low {
            return low;
        }
        if self > high {
            return high;
        }
        return self;
    }
}

// The extended type must be one exact type. `int[..]` gains these methods, but a `uint8[..]` or an
// `int32[..]` does not: it is a different type, and would need an `extend` of its own.
extend int[..] {
    func Sum(self: int[..]) -> int {
        var total = 0;
        for value in self {
            total += value;
        }
        return total;
    }

    func CountEven(self: int[..]) -> uint {
        var count: uint = 0;
        for value in self {
            // A method added above is used like any other.
            if value.IsEven() {
                count += 1;
            }
        }
        return count;
    }
}

func Main() -> int {
    let answer = 42;
    let volume = 130;
    PrintLine("{} is even:        {}", answer, answer.IsEven());
    PrintLine("{} clamped:       {}", volume, volume.Clamped(0, 100));

    let scores: int[5] = [7, 12, 9, 20, 4];
    let all: int[..] = scores;
    PrintLine("sum of all:        {}", all.Sum());
    PrintLine("even scores:       {}", all.CountEven());

    // An array is not a slice, so `scores.Sum()` stops with
    //     error: type 'int[5]' has no field 'Sum'
    // The slice methods are reached through a view of the array instead.
    PrintLine("sum of the last 3: {}", scores[2..].Sum());
    return 0;
}

Run it

cd Examples/Types/Extension
rux run
42 is even:        true
130 clamped:       100
sum of all:        52
even scores:       3
sum of the last 3: 33

Common mistakes

Calling a slice method on an array.
scores.Sum() fails with error: type 'int[5]' has no field 'Sum'. int[5] is an array, a different type from int[..]. Take a view first: scores[..].Sum().
Expecting a method on a related type.
extend int does not reach int32: with let small: int32 = 4;, the call small.Double() fails with error: type 'int32' has no field 'Double'. Likewise a uint8[..] view has no Sum — the error is error: slice type 'uint8[..]' has no member 'Sum', with a note listing the members a slice does have.

Try it yourself

  1. Add func IsPositive(self: int) -> bool to extend int and use it.
  2. Add func Largest(self: int[..]) -> int to the slice extension, and print the largest score.
  3. Add func Average(self: int[..]) -> float64 that reuses Sum. Convert with as before dividing.
  4. Add extend uint8[..] with its own Sum, and call it on a uint8 array through a view.

Learn more

  • Method — extend on your own types
  • Slice — the views these methods work on
  • Methods in the Rux Reference
  • Interface — extend that also makes a promise