Functions · Lesson 4.7

Callback

Source
Pass a named function to another function as a value, typed by its shape like func(int32) -> int32.
You'll need: Function, Return, For

So far every function has been called by name. Functions are also values: one can be stored in a binding, passed to another function, and called from there. A function handed over to be called later is a callback.

That lets one function be given the part that varies. ShowTable below knows how to print a row of results but not what to compute — each caller tells it, by passing a function.

The type of a function

A function's type is its shape: what it takes and what it gives back. It is written like a declaration with the name and parameter names taken out:

FunctionIts type
func Double(value: int32) -> int32func(int32) -> int32
func Square(value: int32) -> int32func(int32) -> int32
func IsEven(value: int32) -> boolfunc(int32) -> bool

Double and Square have different names and different bodies but the same shape, so either fits wherever a func(int32) -> int32 is wanted.

Passing a function

A parameter can have a function type. ShowTable accepts any function with the shape func(int32) -> int32 and calls it once per number:

func ShowTable(last: int32, operation: func(int32) -> int32) {
    for i in 1..=last {
        Print("{} ", operation(i));
    }
    PrintLine();
}

At the call, the function is named but not called — no parentheses after it:

ShowTable(5, Double);
ShowTable(5, Square);
flowchart LR
    main["Main<br/>ShowTable(5, Double)"] --> st["ShowTable<br/>operation = Double"]
    st -- "operation(1) … operation(5)" --> d["Double"]
    d -- "2, 4, 6, 8, 10" --> st

A callback can be called more than once, or have its result fed back into itself:

func ApplyTwice(operation: func(int32) -> int32, value: int32) -> int32 {
    return operation(operation(value));
}

And a callback need not compute a number. CountWhere takes a test — a function returning bool — so it can answer any question that can be written as one:

func CountWhere(last: int32, test: func(int32) -> bool) -> int32 {

A function in a binding

A function value can be held in a binding, with its type written out in full. A var binding can be pointed at another function later; either way it is called exactly like the function it holds:

var chosen: func(int32) -> int32 = Double;
PrintLine("chosen(21)             {}", chosen(21));
chosen = Square;
PrintLine("chosen(21)             {}", chosen(21));

The same call, chosen(21), gives 42 the first time and 441 the second.

The program

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

Src/Main.rux
// Functions have been called by name all along. They are also values: one can
// be stored in a binding, passed to another function, and called from there.
// A function handed over to be called later is a *callback*.
//
// That lets one function be given the part that varies. `ShowTable` below
// knows how to print a row of results but not what to compute; each caller
// tells it, by passing a function.
import Io::{ Print, PrintLine };

func Double(value: int32) -> int32 {
    return value * 2;
}

func Square(value: int32) -> int32 {
    return value * value;
}

func IsEven(value: int32) -> bool {
    return value % 2 == 0;
}

// The parameter's type is the shape of the function: what it takes and what it
// gives back. `func(int32) -> int32` accepts any function with exactly that
// shape, whatever its name.
func ShowTable(last: int32, operation: func(int32) -> int32) {
    for i in 1..=last {
        Print("{} ", operation(i));
    }
    PrintLine();
}

func ApplyTwice(operation: func(int32) -> int32, value: int32) -> int32 {
    return operation(operation(value));
}

// A function taking a test rather than a value can answer any question that
// can be written as one.
func CountWhere(last: int32, test: func(int32) -> bool) -> int32 {
    var found: int32 = 0;
    for i in 1..=last {
        if test(i) {
            found += 1;
        }
    }
    return found;
}

func Main() -> int {
    // `Double` is passed, not called — no parentheses after it. `ShowTable`
    // does the calling, once per number.
    ShowTable(5, Double);
    ShowTable(5, Square);

    PrintLine("ApplyTwice(Double, 3)  {}", ApplyTwice(Double, 3));
    PrintLine("ApplyTwice(Square, 3)  {}", ApplyTwice(Square, 3));
    PrintLine("CountWhere(10, IsEven) {}", CountWhere(10, IsEven));

    // A function value can be held in a binding, with its type written out in
    // full. A `var` binding can be pointed at another function later; either
    // way it is called exactly like the function it holds.
    var chosen: func(int32) -> int32 = Double;
    PrintLine("chosen(21)             {}", chosen(21));
    chosen = Square;
    PrintLine("chosen(21)             {}", chosen(21));

    // Watch out: the shape must match exactly. `ShowTable(5, IsEven)` is
    // refused, because `IsEven` returns `bool`:
    //
    //     error: argument 2 to 'ShowTable' has type 'func(int32) -> bool8',
    //            but parameter 'operation' requires 'func(int32) -> int32'
    //
    // Writing `ShowTable(5, Double(3))` is the other common slip: that calls
    // `Double` and passes its `int32` result, not the function.
    return 0;
}

Run it

cd Examples/Functions/Callback
rux run
2 4 6 8 10
1 4 9 16 25
ApplyTwice(Double, 3)  12
ApplyTwice(Square, 3)  81
CountWhere(10, IsEven) 5
chosen(21)             42
chosen(21)             441

Common mistakes

A function of the wrong shape.
ShowTable(5, IsEven) fails with error: argument 2 to 'ShowTable' has type 'func(int32) -> bool8', but parameter 'operation' requires 'func(int32) -> int32'. The shape must match exactly — bool8 is the full name of bool. The parameter types count too: a func(int) -> int does not fit a func(int32) -> int32 parameter.
Calling the function instead of passing it.
ShowTable(5, Double(3)) calls Double straight away and passes its result, so it fails with has type 'int32', but parameter 'operation' requires 'func(int32) -> int32'. Pass the name alone: ShowTable(5, Double).

Try it yourself

  1. Write Triple and pass it to ShowTable and to ApplyTwice.
  2. Write IsMultipleOfThree and count the multiples of three up to 30 with CountWhere.
  3. Write ApplyTimes(operation: func(int32) -> int32, value: int32, times: int32) -> int32 that applies operation times times in a loop.
  4. Call ShowTable(5, IsEven) and read the error.

Learn more