Eve Callbacks

A callback is a function that you give to other code, so that the other code calls it when it needs it. You write what to do; the receiver decides when. Callbacks make a function general: a loop that applies "something" to every element, a sort that asks "which is first?", a reader that reports its progress.

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.

MethodCallback signatureResult
c.map(f)(x: T): Ua new collection of the same kind with f(x) for every element
c.filter(test)(x: T): Logica new collection with the elements for which test is True
c.reduce(f, start)(acc: U, x: T): Uone 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 ba 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

PatternThe callback isExample
apply to eachnot a callback: a for loop calls a procedurefor x in items do show(x); done;
choosea test that answers True or False (filter)items.filter((x) => (x > 0))
transforma function from one value to another (map)items.map(double)
combinea function of the result so far and one element (reduce)items.reduce((s, x) => (s + x), 0)
ordera comparison of two elements (sort)people.sort((a, b) => (a.age < b.age))
reporta closure that tells when to report progressload(rows, every!(100))
decide on errora handler that returns what to dorare: Eve prefers recover
TODO: explain why Eve prefers recover, defer and jobs to error callbacks, with a comparison to the style of other languages.

Read next: Objects