Generics · Lesson 13.2

Generic method

Source
Give a generic type methods with extend Labeled<T>, including a method with a type parameter of its own.

A generic type gets methods the same way any type does: through extend. The difference is one pair of angle brackets. The block is written extend Labeled<T>, and that T is in scope for every method inside, so a single block serves Labeled<int32>, Labeled<char8[..]> and every other instantiation. A method can also bring a type parameter of its own, chosen afresh at each call.

One extend block for every T

Labeled<T> is a value with a name attached:

struct Labeled<T> {
    label: char8[..];
    value: T;
}

Its methods live in extend Labeled<T>. Inside, T means "whatever this instantiation's T is", and the receivers are written with it — &Labeled<T> to read, &var Labeled<T> to change, exactly as in Mutating method:

extend Labeled<T> {
    // A constructor, as for any struct. It returns the instantiation it was called on.
    func Labeled(label: char8[..], value: T) -> Labeled<T> {
        return Labeled<T> { label: label, value: value };
    }

    func Get(self: &Labeled<T>) -> T {
        return self.value;
    }

    func Set(self: &var Labeled<T>, value: T) {
        self.value = value;
    }

To call the constructor, put the type argument on the type name, and the rest is an ordinary call:

var count = Labeled<int32>("count", 7);
count.Set(15);

From then on count is a Labeled<int32>, so Get returns an int32 and Set accepts only an int32. Nothing is written per type: the compiler produces Get for int32 because count needs it.

A method with a type parameter of its own

With<U> declares a second parameter, U, which has nothing to do with the type's T. T comes from the receiver; U comes from this call's argument:

// `T` comes from the receiver; `U` comes from this call's argument.
func With<U>(self: &Labeled<T>, other: U) -> (T, U) {
    return (self.value, other);
}

The same count can be paired with text on one line and with a bool on the next. U is inferred from the argument, or written out after the method name when you want to be explicit:

let fruit = count.With("apples");
let flag = count.With<bool>(true);
ParameterDeclared onFixed whenIn count.With("apples")
Tthe type, Labeled<T>the value is madeint32
Uthe method, With<U>each call, from its argumentchar8[..]

Changing the instantiation

A method's result can be a different instantiation of the same type. Map keeps the label and turns the value into something else through a callback:

func Map<U>(self: &Labeled<T>, change: func(T) -> U) -> Labeled<U> {
    return Labeled<U> { label: self.label, value: change(self.value) };
}

Here U is inferred from the callback's result type. Half returns a float64 and IsLarge a bool, so the same Labeled<int32> becomes a Labeled<float64> in one call and a Labeled<bool> in the other:

let half = count.Map(Half);
let large = count.Map(IsLarge);
flowchart LR
    c["count<br/>Labeled&lt;int32&gt;"] -- "Map(Half)<br/>Half: int32 → float64" --> h["half<br/>Labeled&lt;float64&gt;"]
    c -- "Map(IsLarge)<br/>IsLarge: int32 → bool" --> l["large<br/>Labeled&lt;bool&gt;"]

The program

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

Src/Main.rux
// A generic type gets methods the same way any type does, through `extend`. The block is
// written `extend Labeled<T>`, and that `T` is in scope for every method inside it: one block
// serves `Labeled<int32>`, `Labeled<char8[..]>` and every other instantiation.
//
// A method may also declare a type parameter of its own, which is chosen at each call and has
// nothing to do with the type's. `With<U>` below pairs the stored `T` with a `U` that can be
// different every time it is called.
import Io::PrintLine;

// A value with a name attached to it.
struct Labeled<T> {
    label: char8[..];
    value: T;
}

extend Labeled<T> {
    // A constructor, as for any struct. It returns the instantiation it was called on.
    func Labeled(label: char8[..], value: T) -> Labeled<T> {
        return Labeled<T> { label: label, value: value };
    }

    func Get(self: &Labeled<T>) -> T {
        return self.value;
    }

    func Set(self: &var Labeled<T>, value: T) {
        self.value = value;
    }

    // `T` comes from the receiver; `U` comes from this call's argument.
    func With<U>(self: &Labeled<T>, other: U) -> (T, U) {
        return (self.value, other);
    }

    // The result can be a different instantiation of the same type: a `Labeled<T>` becomes a
    // `Labeled<U>`, keeping its label and changing its value through `change`.
    func Map<U>(self: &Labeled<T>, change: func(T) -> U) -> Labeled<U> {
        return Labeled<U> { label: self.label, value: change(self.value) };
    }
}

func Half(value: int32) -> float64 {
    return (value as float64) / 2.0;
}

func IsLarge(value: int32) -> bool {
    return value > 100;
}

func Main() -> int {
    // The type argument goes on the type name, then the constructor runs as usual.
    var count = Labeled<int32>("count", 7);
    count.Set(15);
    PrintLine("{} = {}", count.label, count.Get());

    // `U` is inferred from the argument, or written out after the method name.
    let fruit = count.With("apples");
    let flag = count.With<bool>(true);
    PrintLine("with     {} {}", fruit.0, fruit.1);
    PrintLine("with     {} {}", flag.0, flag.1);

    // `U` inferred from the callback's result type.
    let half = count.Map(Half);
    let large = count.Map(IsLarge);
    PrintLine("{} / 2 = {}", half.label, half.Get());
    PrintLine("{} > 100 is {}", large.label, large.Get());
    return 0;
}

Run it

cd Examples/Generics/GenericMethod
rux run
count = 15
with     15 apples
with     15 true
count / 2 = 7.5
count > 100 is false

Common mistakes

Writing extend Labeled without its parameter.
extend Labeled { … } fails with error: struct type 'Labeled' requires 1 type argument, but 0 were provided, followed by error: type 'T' is not defined in this scope for every method that mentions T. The block must declare the parameter it uses: extend Labeled<T>.
Calling the constructor without a type argument.
Labeled("count", 7) fails with error: constructor for 'Labeled' requires 1 type argument, but 0 were provided. Write Labeled<int32>("count", 7).
Changing a let binding.
Set takes &var Labeled<T>, so it needs a var. With let count = …, count.Set(15) fails with error: cannot call 'Set' on immutable 'count' — generics change nothing about mutability.

Try it yourself

  1. Make a Labeled<char8[..]> called "name" holding "Ada", and print it through Get.
  2. Add func Relabel(self: &Labeled<T>, label: char8[..]) -> Labeled<T>, which returns a copy with a new label.
  3. Write a function Describe(value: int32) -> char8[..] that returns "small" or "large", and pass it to Map. What type is the result?
  4. Call count.With(count). What are T and U, and what does the tuple hold?

Learn more