Callback
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:
| Function | Its type |
|---|---|
func Double(value: int32) -> int32 | func(int32) -> int32 |
func Square(value: int32) -> int32 | func(int32) -> int32 |
func IsEven(value: int32) -> bool | func(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" --> stA 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.
// 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
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.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
- Write
Tripleand pass it toShowTableand toApplyTwice. - Write
IsMultipleOfThreeand count the multiples of three up to 30 withCountWhere. - Write
ApplyTimes(operation: func(int32) -> int32, value: int32, times: int32) -> int32that appliesoperationtimestimes in a loop. - Call
ShowTable(5, IsEven)and read the error.
Learn more
- Function type aliases in the Rux Reference
- Generic — the other way to give one function a part that varies
- Function field — storing a function inside a struct