Types · Lesson 6.13

Function field

Source
Store a function in a struct field, so a value carries its behaviour with it.
You'll need: Callback, Method

The Callback lesson passed a function as an argument and held one in a local. A struct field can hold one too. A value then carries its behaviour with it: an operation knows its symbol and also how to compute, and code that loops over operations needs to know neither in advance.

A field of function type

The field's type is a function type, exactly as for a parameter — the parameter types in parentheses, then the return type:

struct Operation {
    symbol: char8[..];
    apply: func(int, int) -> int;
}

Any function that takes two ints and returns an int fits in apply: Add, Subtract, Multiply and Larger all do.

Storing a function

A function is stored by its name, without parentheses — as when passing one to a callback parameter:

Operation { symbol: "+", apply: Add },

Add is the function itself; Add(…) would call it and store the result, which is an int, not a function.

Calling through the field

Calling a field looks like calling a method, but apply is data: whatever function was stored in it is what runs.

func Show(self: &Operation, a: int, b: int) {
    PrintLine("{} {} {} = {}", a, self.symbol, b, self.apply(a, b));
}
flowchart LR
    invoke["`operation.Show(7, 3)`"] --> field["`self.apply(7, 3)`"]
    field --> q{"What is stored<br/>in `apply`?"}
    q -- "`Add`" --> a["`10`"]
    q -- "`Multiply`" --> m["`21`"]
    q -- "`Larger`" --> l["`7`"]

The loop that prints all four lines names none of the four functions:

for operation in operations {
    operation.Show(7, 3);
}

Adding a fifth operation means adding one line to the array — the loop does not change.

Method or function field?

A method ShowA function field apply
Declared once in extend OperationSet separately in every value
The same code for every OperationDifferent code in each Operation
Cannot be changedAssigned like any field, on a var

A function field is assigned like any other field, which changes what the value does from then on:

var changing = Operation { symbol: "?", apply: Add };
changing.Show(6, 4);
changing.apply = Multiply;
changing.Show(6, 4);

The program

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

Src/Main.rux
// The Callback lesson passed a function as an argument and held one in a local. A struct field
// can hold one too. A value then carries its behaviour with it: an operation knows its symbol and
// also how to compute, and code that loops over operations needs to know neither in advance.
//
// The field's type is a function type, exactly as for a parameter: `func(int, int) -> int`.
import Io::PrintLine;

func Add(a: int, b: int) -> int {
    return a + b;
}

func Subtract(a: int, b: int) -> int {
    return a - b;
}

func Multiply(a: int, b: int) -> int {
    return a * b;
}

func Larger(a: int, b: int) -> int {
    return a > b ? a : b;
}

struct Operation {
    symbol: char8[..];
    apply: func(int, int) -> int;
}

extend Operation {
    // Calling a field looks like calling a method, but `apply` is data: whatever function was
    // stored in it is what runs.
    func Show(self: &Operation, a: int, b: int) {
        PrintLine("{} {} {} = {}", a, self.symbol, b, self.apply(a, b));
    }
}

func Main() -> int {
    // A function is stored by name, without parentheses, as when passing one.
    let operations: Operation[4] = [
        Operation { symbol: "+", apply: Add },
        Operation { symbol: "-", apply: Subtract },
        Operation { symbol: "*", apply: Multiply },
        Operation { symbol: "max", apply: Larger }
    ];

    // One loop, four behaviours. Nothing in it names any of the four functions.
    for operation in operations {
        operation.Show(7, 3);
    }

    // The field is called directly from outside a method too.
    let first = operations[0];
    PrintLine("through the field: {}", first.apply(20, 22));

    // A function field is assigned like any other field, which changes what the value does.
    var changing = Operation { symbol: "?", apply: Add };
    changing.Show(6, 4);
    changing.apply = Multiply;
    changing.Show(6, 4);
    return 0;
}

Run it

cd Examples/Types/FunctionField
rux run
7 + 3 = 10
7 - 3 = 4
7 * 3 = 21
7 max 3 = 7
through the field: 42
6 ? 4 = 10
6 ? 4 = 24

Common mistakes

Try it yourself

  1. Write func Smaller(a: int, b: int) -> int and add a min operation to the array. Nothing else should need to change.
  2. Write func Power(a: int, b: int) -> int with a loop, and add ^ to the array.
  3. Name the function type with an alias, type Binary = func(int, int) -> int;, and use it in the struct.
  4. Give Operation a second function field, check: func(int, int) -> bool, that says whether the operation is safe for its arguments — for example, a division that refuses a zero divisor.

Learn more