Eve Callbacks
The idea
A parameter can have the type Function, or a more exact function type (see below). The caller passes a function: a named function, a lambda expression or a closure. Inside, the receiver calls the parameter like any function.
# a callback passed by name
driver callback_demo is
** the function to hand over
function double(x: Integer) => (@y: Integer) is
let y := x * 2;
return;
** the receiver calls "action" for every element
procedure each(items: ()Integer, action: Function) is
for x in items do
print action(x);
done;
return;
process main is
each((1, 2, 3), double); ** 2 4 6
each((1, 2, 3), (x) => (x + 100)); ** a lambda: 101 102 103
return;
end callback_demo;
The type of a callback
A function signature can be declared as a class derived from Function, with the operator <:. The signature fixes the parameter list and the result, and leaves the body to be written later. The class is then used as the type of a call-back parameter, variable or collection element. The compiler checks every function passed to such a parameter: a function whose signature does not match is rejected at compile time.
# define a lambda signature
driver lam_sig is
** Define callback signature
class BinEx = (p1, p2 :Integer):Integer <: Function;
** Use lambda signature as parameter type
procedure useLambda(test :BinEx) is
print test(1, 2); ** execute callback
return;
** Execute a process with lambda argument
process main is
** use anonymous lambda argument
useLambda((x, y) => (x + y));
return;
end lam_sig;
Note: Lambda expressions can be created by a function or method as a result. Lambda expressions can't modify values of variable declared in the parent scope.
Note: A named function is passed by its name, without parentheses: each(items, double). With parentheses, double(2), it is a call and its result is passed. The type Function alone accepts any function; a class such as BinEx accepts only the functions with its signature.
Note: A callback is always a function. Eve holds no references to procedures and no references to methods: a procedure or a method can't be passed as an argument, stored in a variable or returned, and there are no procedure types and no method types. A procedure may receive a callback, like each above, but it is never one. To use a method of an object in a callback, pass a lambda that calls it: shapes.map((s) => (s.area())).
A callback with a state
A callback that must remember something is a closure: the function that creates it holds the state (see Closures). A closure is always a function, and the function that creates it always ends with !. A progress counter is the usual example: the closure counts the steps and says when it is time to report; the receiver, a procedure, does the printing.
** the signature of the progress test
class Due = (): Logic <: Function;
** creates a closure that answers True every n-th call
function every!(n: Integer) => (@due!: Due) is
new seen := 0;
function due!() => (@yes: Logic) is
let seen += 1;
let yes := seen % n == 0;
return;
return;
** the receiver asks the callback, and prints by itself
procedure load(rows: ()String, due!: Due) is
loop
new i := 0;
for r in rows do
let i += 1;
print "done: {i}" if due!();
done;
return;
A call is load(rows, every!(100)): it prints a line after every hundred rows.
Collections and callbacks
The collections (List, Array, DataSet, DataMap) have methods that take a callback. They do the loop, the callback, a function, says what to do with one element. Every method returns a new collection or a value and does not change the collection it is called on. There is no each method: work done for every element is a procedure, and a procedure is not a callback, so it is called in a for loop.
| Method | Callback signature | Result |
|---|---|---|
c.map(f) | (x: T): U | a new collection of the same kind with f(x) for every element |
c.filter(test) | (x: T): Logic | a new collection with the elements for which test is True |
c.reduce(f, start) | (acc: U, x: T): U | one value: f is applied to start and the first element, then to that result and the next element, and so on |
c.sort(by) | (a, b: T): Logic, True when a goes before b | a new List in the order given by by |
For a DataMap the element is a pair (key: value); map and filter keep the keys.
Example: orders
# callbacks on a list of orders
driver order_report is
class Order = {id: Integer, total: Decimal, paid: Logic} <: Object;
** a test: one order in, True or False out
function is_paid(o: Order) => (@r: Logic) is
let r := o.paid;
return;
** a procedure: it prints one order (not a callback: it is called in a loop)
procedure show(o: Order) is
print "order {o.id}: {o.total}";
return;
process main is
new orders := (Order(id: 1, total: 12.50d, paid: True),
Order(id: 2, total: 99.00d, paid: False),
Order(id: 3, total: 5.25d, paid: True));
** filter: keep the paid orders (a named function)
new paid := orders.filter(is_paid);
** map: the totals only (a lambda)
new totals := paid.map((o) => (o.total));
** reduce: the sum, starting from zero
new sum := totals.reduce((acc, t) => (acc + t), 0.00d);
expect sum == 17.75d;
** sort: the biggest order first
new ranked := orders.sort((a, b) => (a.total > b.total));
** print every order: a for loop calls the procedure
for o in ranked do
show(o);
done;
return;
end order_report;
order 2: 99.00 order 1: 12.50 order 3: 5.25
Rules
- A callback runs on the core of the receiver, in serial mode, at the point where it is called;
- A callback is a function: a named function, a lambda or a closure. A procedure or a method is never a callback;
- A parameter typed with a function class (
BinEx,Due) accepts only functions with that signature; the check is done at compile time; - A callback that changes state is stochastic and ends with
!; the receiver declares the parameter with the same mark; - An error raised in a callback goes to the receiver and from there to the caller, like any error in a function;
- A callback can't be passed to
start: parallel work is done by aspects, not by callbacks (see Concurrency); - Prefer a plain function or a lambda to a closure: they have no state to get wrong.
Patterns
| Pattern | The callback is | Example |
|---|---|---|
| apply to each | not a callback: a for loop calls a procedure | for x in items do show(x); done; |
| choose | a test that answers True or False (filter) | items.filter((x) => (x > 0)) |
| transform | a function from one value to another (map) | items.map(double) |
| combine | a function of the result so far and one element (reduce) | items.reduce((s, x) => (s + x), 0) |
| order | a comparison of two elements (sort) | people.sort((a, b) => (a.age < b.age)) |
| report | a closure that tells when to report progress | load(rows, every!(100)) |
| decide on error | a handler that returns what to do | rare: Eve prefers recover |
recover, defer and jobs to error callbacks, with a comparison to the style of other languages.Read next: Objects