6.031
6.031 — Software Construction
Spring 2022

Reading 20: Callbacks and Graphical User Interfaces

Software in 6.031

Safe from bugsEasy to understandReady for change
Correct today and correct in the unknown future. Communicating clearly with future programmers, including future you. Designed to accommodate change without rewriting.

Objectives

In this reading we talk about callbacks, in which an implementer calls a function provided by the client. We discuss this idea in the context of graphical user interfaces (GUIs), where a kind of callback function called a listener is used to respond to incoming input events from the user.

We’ll see more instances of callbacks in future classes, when we talk about web servers and asynchronous computation.

Callbacks

A callback is a function that a client provides to a module for the module to call. This is in contrast to normal control flow, in which the client is doing all the calling: calling down into functions that the module provides. With a callback, the client is providing a piece of code for the implementer to call.

We already saw one kind of callback in Map/Filter/Reduce: the function passed to map or filter or reduce, which is called repeatedly for each element of the sequence. This is a synchronous callback, because it is called only during the execution of the map/filter/reduce, and then not used thereafter. The use of the callback is synchronized within the call and return of the map/filter/reduce.

This reading considers asynchronous callbacks, which are saved by the module for a future call, after the function that received the callback has already returned.

Here’s one analogy for thinking about the difference between synchronous and asynchronous. Normal (synchronous) function calling is like picking up the phone and calling a service, like calling your bank to find out the balance of your account. You give the information that the bank operator needs to look up your account, they read back the account balance to you over the phone, and you hang up. You are the client, and the bank is the module that you’re calling into.

Sometimes the bank is slow to give an answer. You’re put on hold, and you wait until they figure out the answer for you. This is like a synchronous function call that blocks until it is ready to return.

But sometimes the task may take so long that the bank doesn’t want to put you on hold. Then the bank will ask you for a callback phone number, and they will promise to call you back with the answer, at some unpredictable point in the future. This is analogous to providing an asynchronous callback function.

Sometimes a callback function will be called only once (or at most once), as a response to a one-time request, as in this account-balance example. But a callback function may also be called repeatedly, whenever a certain event happens. An analogy for this kind of callback might be account fraud protection, where the bank calls you at your registered phone number whenever a suspicious transaction occurs on your account. We will see both kinds of asynchronous callbacks in this reading.

Timer callbacks

A simple example that uses a callback is the built-in JavaScript function setTimeout, which takes a callback function and a number of milliseconds, and calls the function after the specified time has elapsed:

setTimeout(() => console.log("beep!"),
           3000);

Here the callback function is the function object created by the lambda expression () => console.log("beep!").

Running this code at a TypeScript prompt would produce:

> setTimeout(() => console.log("beep!"), 3000);
>
3 seconds later
beep!

Notice that the callback is asynchronous. The call to setTimeout() itself doesn’t wait for 3000 milliseconds (3 seconds) before returning. Instead, it starts an internal timer, stores a reference to the callback function, and then returns right away, and the next TypeScript prompt > is printed to wait for the user to type another expression. Once the internal timer expires, three seconds later, then the callback function is finally called, printing beep!.

A side note: setTimeout is unusual for expecting its callback function as the first argument. It’s an unfortunate design choice, since when a call to setTimeout is written on one line with a lambda expression (as in the example just above), it can be hard to tell where the lambda expression ends and the remaining arguments to setTimeout start. Usually a callback function appears at the end of a parameter list, and the remaining examples in this reading all use that convention instead.

To see an example of defining a function that takes a callback, let’s create a countdown timer:

/**
 * Start a timer that ticks once per second until it expires.
 * @param ticks      duration of the timer in seconds, must be an integer >= 0
 * @param callback   callback function, initially called synchronously, 
 *                   then called again after each tick of the timer, 
 *                   each time passing the number of seconds left until the timer expires.
 *                   ticksLeft must be an integer in [0, ticks].
 */
function countdown(ticks: number, callback: (ticksLeft: number) => void): void

Look closely at the type of the callback parameter: this is a function type. In this case, countdown() is expecting a callback function that takes a single number argument and returns void.

Here’s how it might be called at a TypeScript prompt:

> countdown(3, (n:number) => console.log(n) );
3
>
one second later
2
one second later
1
one second later
0

Notice where 3 is printed in the transcript above: before the next prompt > was displayed, because the spec indicates that the first callback should be called synchronously, before countdown() returns. The remaining calls to the callback happen asynchronously, as the timer counts down to completion.

The exercises below explore the behavior of the setTimeout() and countdown() callbacks, and show how to implement countdown() using setTimeout().

reading exercises

Time to print
setTimeout(() => console.log("A"), 4000);
setTimeout(() => console.log("B"), 1000);
console.log("C");

In what order are A, B, and C printed by this code?

(missing explanation)

What is the total duration of the program, from the start of the code to the final line printed, rounded to the nearest second?

(missing explanation)

Callback type

Recall the function signature for countdown, with particular attention to the callback parameter:

function countdown(ticks: number, callback: (ticksLeft: number) => void): void

and suppose the client has previously defined this function:

function myCallback(ticksLeft:number) { console.log(ticksLeft); }

Which of the following are true statements about the callback that the client can pass to countdown?

(missing explanation)

(missing explanation)

Counting down

Let’s consider how countdown() might be implemented using setTimeout(). Here is a recursive implementation:

/**
 * Start a timer that ticks once per second until it expires.
 * @param ticks      duration of the timer in seconds, must be an integer >= 0
 * @param callback   callback function, initially called synchronously, 
 *                   then called again after each tick of the timer, 
 *                   each time passing the number of seconds left until the timer expires.
 *                   ticksLeft must be an integer in [0, ticks].
 */
function countdown(ticks: number, callback: (ticksLeft: number) => void): void {
    const millisecondsPerTick = 1000;
    if ( ticks > 0 ) {
        setTimeout(_____________, millisecondsPerTick);
    }
    callback(ticks);
}

Which of the following expressions could be put in the blank for the recursive case, so that the implementation works for ticks > 0?

(missing explanation)

What does this code do when called with countdown(0, (ticksLeft) => console.log(ticksLeft))?

(missing explanation)

Counting down again

Now let’s consider an imperative implementation of countdown(), which is currently buggy:

    /**
     * Start a timer that ticks once per second until it expires.
     * @param ticks      duration of the timer in seconds, must be an integer >= 0
     * @param callback   callback function, initially called synchronously, 
     *                   then called again after each tick of the timer, 
     *                   each time passing the number of seconds left until the timer expires.
     *                   ticksLeft must be an integer in [0, ticks].
     */
    function countdown(ticks: number, callback: (ticksLeft: number) => void): void {
        const millisecondsPerTick = 1000;

        function doTick() {
            callback(ticks);
            if ( ticks > 0 ) {
                setTimeout(doTick, millisecondsPerTick);
            }
        }

        doTick();
    }

What does this code do when called with countdown(0, (ticksLeft) => console.log(ticksLeft))?

(missing explanation)

What does this code do when called with countdown(1, (ticksLeft) => console.log(ticksLeft))?

(missing explanation)

Counting down again, but not ad infinitum

The bug in the iterative implementation is that it never decrements ticks. Let’s try fixing it:

    /**
     * Start a timer that ticks once per second until it expires.
     * @param ticks      duration of the timer in seconds, must be an integer >= 0
     * @param callback   callback function, initially called synchronously, 
     *                   then called again after each tick of the timer, 
     *                   each time passing the number of seconds left until the timer expires.
     *                   ticksLeft must be an integer in [0, ticks].
     */
    function countdown(ticks: number, callback: (ticksLeft: number) => void): void {
        const millisecondsPerTick = 1000;
//A
        function doTick() {
//B
            callback(ticks);
//C
            if ( ticks > 0 ) {
//D
                setTimeout(doTick, millisecondsPerTick);
//E
            }
        }
//F
        doTick();
//G
    }

Suppose you insert the statement --ticks; at just one of the locations marked A-G. Which of the possible insert locations would fix the bug, so that countdown() obeys its spec?

(missing explanation)

Event loop

These timer callbacks allow us to uncover a fundamental aspect of the TypeScript/JavaScript programming model. At the core of the JavaScript runtime system is an event queue, a first-in-first-out queue that stores events that have arrived from various sources. The queue is serviced by an event loop that repeatedly checks this queue for newly-arrived events, and calls appropriate callback functions.

One source of events are the timers created by setTimeout. The expiration of a timer causes a “timer done” event to be placed at the end of the queue, along with its callback function. When the event loop eventually retrieves this event from the top of the queue, it calls the callback function, finally discharging the obligation of setTimeout’s contract.

Later in this reading, we will see another source of events: a graphical user interface. For JavaScript running in an HTML web page, the user’s keypresses, mouse movements and presses, or finger-touches are placed at the end of the event queue, and eventually handled by callback functions.

And still another source of events are input and output to the filesystem and network, which we will see in a future reading.

All our TypeScript/JavaScript code is actually running inside the event loop — even the start of the program is initiated from the event loop. In order for the program to work correctly, all our code must also return to the event loop in a timely way, so that the event loop can move on to the next event, and continue processing subsequent timer expirations or GUI/file/network events.

If any of our code blocks — holding onto the flow of control and not returning to the caller for a significant period of time — then the event loop stops processing events. Timer callbacks don’t fire, the user interface stops responding, and network connections stop delivering data. If you have ever seen your mouse cursor turn into a spinning beach ball or an hourglass, that’s a sign of a program whose event loop has become blocked, so that it is no longer processing events. It’s bad.

Here’s a simple function that would block the JavaScript event loop:

/** Wait for `milliseconds` to pass before returning. */
function busyWait(milliseconds: number): void {
    const now = new Date().getTime(); // new Date().getTime() is always the current clock time in milliseconds
    const deadline = now + milliseconds;
    while (new Date().getTime() < deadline) {
        // do nothing, just wait until the clock reaches the deadline time
    }
}

A call to busyWait(10000) spins and spins around its while loop, watching the clock, until 10 seconds have passed. During that time, events will pile up in the event queue, but the event loop will not be able to process them, because busyWait() has not returned. Once busyWait() returns its caller (which then returns to its caller, and so forth all the way back up to the event loop, which is the original caller of everything in JavaScript), then the event loop can finally peel off the next waiting event and call its callback function, and life can return to normal. But all those timer callbacks that piled up will fire too late!

Spinning in a loop waiting for something to change externally is called busy-waiting, and it is a very bad code smell for an event-loop runtime system like JavaScript. Code like busyWait() needs to be rewritten. It should use a callback triggered by the change, as setTimeout() does.

reading exercises

Blocking the event loop

The busyWait() function is definitely a bad design for TypeScript/JavaScript. But even well-meaning functions can inadvertently block the event loop:

/** @returns a random prime number with `n` decimal digits. */
function randomPrime( n: number ): bigint;

Suppose randomPrime() is implemented by a guess-and-test strategy, generating n-digit numbers at random and testing them for primality until it finds a prime one. So randomPrime(1000) may take an unpredictable amount of time to run.

Which possible behaviors might you see from the example code below? Choose all that apply.

setTimeout(() => console.log("A"), 1000);
console.log(randomPrime(1000));
setTimeout(() => console.log("B"), 2000);

(missing explanation)

Exceptions from synchronous callbacks

One difference between synchronous and asynchronous callbacks is how they behave with exception handling.

Let’s first consider a synchronous callback that always throws an exception, used by map:

function alwaysThrows(n:number) { 
    throw new Error("boom!");
}

[1, 2, 3].map(alwaysThrows);

Which of the following places to put a try-catch statement will catch the exception thrown by alwaysThrows? Assume the choices are independent, i.e. the try-catch is put in only one place in the code.

(missing explanation)

Exceptions from asynchronous callbacks

Now let’s consider an asynchronous callback, used by setTimeout:

function alwaysThrows() { 
    throw new Error("boom!");
}

setTimeout(alwaysThrows, 1000);

Which of the following places to put a try-catch statement will catch the exception thrown by alwaysThrows? Assume the choices are independent, i.e. the try-catch is put in only one place in the code.

(missing explanation)

HTML and the Document Object Model

Asynchronous callbacks are heavily used in graphical user interfaces. To understand how, we need to start by looking at how user interfaces are structured, using HTML web pages as an example.

HTML consists of elements, which are delimited by a start tag and end tag. For example, here is how a button is described in HTML:

<button id='drawButton'>Draw</button>

… which the web browser renders like this:

The start tag often contains attributes, like id here. The id attribute is a unique identifier for the element. Other attributes may specify other properties of the element. For example, here is a 256-by-256-pixel drawing surface:

<canvas id="drawingCanvas" width="256" height="256"></canvas>

(The <canvas> element has the Canvas drawing interface that we are using in ps3.)

HTML elements are nested in other HTML elements, to form a tree. Below is a snippet of HTML that groups two buttons together in one row, and the canvas in another row, using the <div> grouping element. The way it might render in a web page is shown on the right.

<div>
    <button id="drawButton">Draw</button>
    <button id="clearButton">Clear</button>
</div>
<div>
    <canvas id="canvas" width="256" height="256">
    </canvas>
</div>

The tree of elements is called the Document Object Model (DOM). Each element of the tree occupies a certain portion of the screen, generally a rectangular area called its bounding box. The tree is not just an arbitrary hierarchy, but is in fact a spatial one: child elements are typically nested inside their parent’s bounding box.

Virtually every GUI system has some kind of tree of visual elements. The element tree is a powerful structuring idea, which is loaded with responsibilities in a typical GUI:

Output. Attributes and children of the element affect what the element displays on the screen. For example, the label on the button is the text found between its start tag and end tag.

Input. Elements can have input handlers that respond to mouse and keyboard input from the user, and to other events. More on this in a moment.

Layout. The DOM controls how elements are laid out on the screen, i.e. how their bounding boxes are assigned. An automatic layout algorithm automatically calculates the position and size of each element, controlled by style rules written in Cascading Style Sheets (CSS). CSS layout is outside the scope of what we’ll be talking about in 6.031; 6.170 or IAP Web.Lab are good places to learn it.

Connecting to TypeScript

To make an interactive user interface, HTML can also include JavaScript code (or TypeScript which has been compiled into JavaScript). The code can obtain references to elements in the tree using document.getElementById():

const drawButton: HTMLButtonElement = document.getElementById('drawButton');

DOM elements are mutable, so the code can reassign instance variables or call mutators to add and remove elements from the tree, or change their appearance. For example, this code changes the button’s label:

drawButton.text = 'Draw Randomly';

Input handling

Input is handled differently in GUIs than in typical command-line or text-input user interfaces. A typical text UI has an input loop that displays a prompt, reads a command typed by the user, parses it, and decides how to direct it to different modules of the program.

If a GUI were written that way, it might look like this (in pseudocode):

while (true) {
    read mouse click
    (x,y) = position of mouse click
    if (mouse was clicked on the Draw button) drawRandomShape();
    else if (mouse was clicked on the Clear button) clearCanvas();
    else if (mouse was clicked somewhere on the canvas) drawShapeAtPosition(x, y);
    ...
}

But in a GUI, we don’t directly write this kind of code, because it’s not modular. GUIs are put together from a variety of library components – buttons, scrollbars, textboxes, menus – that need to be self-contained and handle their own input. Deep inside the graphical user interface library is an input loop that is reading from the mouse and keyboard, and passing those input events to the appropriate components of the GUI.

In order to make a button do something when it is clicked, you attach a listener to it:

drawButton.addEventListener('click', (event:MouseEvent) => {
  drawRandomShape();
});

The second argument to this method is a function expression using arrow syntax, which we saw in a previous class.

GUI event handling is an instance of the Listener pattern. In the Listener pattern:

  • An event source generates a stream of discrete events.
  • One or more other modules subscribe (or listen) to the stream of events, providing a function to be called when a new event occurs.

In this example:

  • the button is the event source;
  • its events are button presses;
  • the listener is a function provided by the client.

An event often includes additional information which might be bundled into an event object (like the MouseEvent here) or passed as parameters to the listener function.

When an event occurs, the event source distributes it to all subscribed listeners, by calling their listener callbacks.

So the control flow through a graphical user interface proceeds like this:

  • The top-level event loop reads input from mouse and keyboard. In HTML/JavaScript, like most other GUI frameworks, this loop is actually hidden from you. It’s buried inside the GUI library, and listeners appear to be called magically.
  • Each listener does its thing (which might involve e.g. mutating objects in the user interface), and then returns immediately to the event loop.

The last part – listeners return to the event loop as fast as possible – is very important, because it preserves the responsiveness of the user interface.

The Listener design pattern isn’t just used for button presses. Every GUI object generates events, often as a result of some combination of low-level input events sent by input devices, like a mouse, keyboard, or touchscreen. For example, in HTML:

  • the mouse sends low-level events mousedown (the mouse button was pressed down) and mouseup (when the button was released)
  • the keyboard sends similar low-level events keydown and keyup when keyboard keys are pressed and released
  • a button (<button>) sends a click event after it receives both mousedown and mouseup.
  • a dropdown menu (<select>) sends an input event when the user selects a different element from the dropdown menu (whether by mouse or by keyboard)
  • a textbox (<input type=text>) sends an input event when the text inside it changes, as a result of keyboard or mouse events.

reading exercises

Listeners

Put the following items in order according to when they would happen during the setup and execution of an HTML graphical user interface.

const launchButton: HTMLButtonElement = document.getElementById('launchButton');

launchButton.addEventListener('click', launchMissiles);

launchMissiles() function is called

HTML code <button id="launchButton"> is interpreted by the web browser to create an HTMLButtonElement object in the DOM

Mouse click on the launch button is received by the input event loop

(missing explanation)

Don’t call me, I’ll call you

The listener in this section is an example of a callback, where the client of a module provides a piece of code for that module to call:

drawButton.addEventListener('click', (event:MouseEvent) => {
  drawRandomShape();
});

What is the client in the example?

(missing explanation)

What is the piece of code in the example?

(missing explanation)

What is that module in the example?

(missing explanation)

Event sources

The listener pattern is a general programming idea, and there are two ways it appears in TypeScript/JavaScript programming:

  • in web pages, as we have just seen, objects that can act as event sources implement the addEventListener() method, which is part of the EventTarget interface.

  • in Node, event sources implement the on() method, which is part of EventEmitter. And addListener() is a synonym for on().

The addEventListener() and on()/addListener() methods have essentially the same parameters: a string naming the type of the event, and a callback function that will be called when the event occurs.

Pitfalls in listeners

There are a few common pitfalls in the listener design pattern, particularly when used in GUIs, that it’s important to be aware of.

Listeners should run quickly and return quickly. If a listener function responds to an event by doing a long, slow computation, then it will stall the user interface. Since the listener was called by the GUI event loop, the event loop will not be able to continue processing input events until the listener returns.

For example, suppose the draw button’s listener drew a large number of random shapes:

drawButton.addEventListener('click', (event:MouseEvent) => {
    for (let i = 0; i < 1000*1000; i++) {
        drawRandomShape();
    }
});

Drawing a million rectangles may involve a long, long pause. During that time, the web page appears frozen. Mouse clicks don’t seem to work, scrolling doesn’t work, keypresses don’t seem to be handled. On many platforms, the operating system changes the mouse cursor to an hourglass or colorful spinning beachball to show that the user interface is stuck.

Once the million rectangles are finally all drawn, and the listener function returns, then the event loop resumes processing. All the input events you tried to do while it was stuck were stored in a queue, so it will eventually catch up.

The lesson here is: in graphical user interaces, listener functions must run quickly and return quickly, typically within a few milliseconds. If a listener needs to do a long-running computation, it should use concurrency, which we will talk about in upcoming classes.

Be careful about cycles. Sometimes it is necessary to have two GUI elements that mutually depend on each other, like the textbox and slider in the code below:

Size:
<div>
    Size:
    <input type="number" id="sizeTextbox" value="10" min="1" max="50" maxlength="2"> 
    <input type="range" id="sizeSlider" value="10" min="1" max="50">
</div>

In this design, the user should be able to change the size either by typing a number in the box, or by dragging the slider. Either way of doing it should update the other element as well, so that they remain consistent. So it’s tempting to attach listeners like this, to propagate the value from the textbox to the slider, and vice versa:

sizeTextbox.addEventListener('input', (event:ChangeEvent) => {
    // make the slider match the updated textbox
    sizeSlider.value = sizeTextbox.value;
});
sizeSlider.addEventListener('input', (event:ChangeEvent) => {
    // make the textbox match the updated slider
    sizeTextbox.value = sizeSlider.value;
});

This creates a cycle of listeners. If the textbox changes, its listener responds by mutating the slider, which might in turn trigger the listener for the slider, which mutates the textbox, which might then trigger the textbox listener again. There is a risk of an infinite loop.

In this case, the cycle is broken by specification: the input event is specified to be sent only when the user changes the value of the textbox or slider, using mouse or keyboard input. Mutations of the value caused by code do not trigger the event. So there is no risk of infinite loop in this particular case.

Implementing an event source

So far we have seen listeners only from the client’s point of view. The client creates a callback function, passes it to the implementer, and when the desired event occurs, the implementer calls it.

What about the implementer’s side? How does an implementer keep track of listeners, and call them when an event occurs? To illustrate this, we’ll implement a simple counter that calls its listeners whenever it increments.

Counter spec

Counter has several operations:

class Counter {

    /** Make a counter initially set to zero. */
    public constructor()  { ... }

    /** @returns the value of this counter. */
    public get value():bigint  { ... }

    /** Increment this counter. */
    public increment():void  { ... }

    /** Modifies this counter by adding a listener.
      * @param listener called by this counter when it changes. */
    public addEventListener(listener: (numberReached:bigint) => void):void  { ... }

    /** Modifies this counter by removing a listener.
      * @param listener will no longer be called by this counter. */
    public removeEventListener(listener: (numberReached:bigint) => void):void  { ... }
}

From a client’s point of view, we can make a counter and attach listeners to it:

const counter = new Counter();
counter.addEventListener((value:bigint) => console.log(value));

Whenever counter.increment() is called, Counter will call this listener, which will print the new number.

Counter implementation

Let’s turn to the implementer’s view. Counter needs to track both the number it is counting and the set of listeners who want to be called back. So we’ll use this straightforward rep:

private _value: bigint = 0n;
private listeners: Array<(numberReached:bigint) => void> = [];


// Abstraction function
//    AF(_value, listeners) = a counter currently at `_value`
//       that sends events to the `listeners` whenever it changes
// Rep invariant
//    true

The counting itself is handled by increment():

public increment():void {
    ++this._value;
    this.callListeners();
}

The essence of implementing the Listener pattern is the cooperation between the public mutators addEventListener() and removeEventListener(), which clients use to register/unregister their callback functions, and the private method callListeners() that the implementation uses to call those callback functions. The operations might then be implemented like this:

public addEventListener(listener: (numberReached:bigint) => void):void {
    this.listeners.push(listener);
}

public removeEventListener(listener: (numberReached:bigint) => void):void {
    // search backwards through the array,
    // so we can remove all matches to `listeners`
    // without having to adjust the index
    for (let i = this.listeners.length-1; i >= 0; --i) {
        if (this.listeners[i] === listener) {
            this.listeners.splice(i, 1);
        }
    }
}

private callListeners():void {
    for (const listener of this.listeners) {
        listener(this._value);
    }
}

In this code, addEventListener()/removeEventListener() maintain the set of listeners. Whenever the counter increments, it calls callListeners() to iterate through the set, calling each listener.

Unfortunately this straightforward implementation has a subtle bug. The following reading exercises uncover the bug and fix it.

reading exercises

Calling a listener

Let’s look closely at the type of the listeners field in the rep for Counter:

private listeners: Array<(numberReached:bigint) => void> = [];

Suppose Counter wants to call the ith listener on this array, passing it the value n as the counter value reached. Which of the following expressions does that correctly?

(missing explanation)

What is the type of this call expression?

(missing explanation)

A one-time listener

Suppose a client of Counter only cares to be called back once, and then removes itself to stop further calls:

function oneShotListener(value:bigint) {
  console.log(value);
  counter.removeEventListener(oneShotListener);
}
counter.addEventListener(oneShotListener);

Assume that addEventListener(), removeEventListener(), and callListeners() are implemented as shown above, and that the counter has multiple listeners attached to it, not just this one.

The counter increments, and Counter now calls all its listeners, synchronously. Which of the following is likely to happen when oneShotListener is called?

(missing explanation)

Summary

Callbacks are another example of first-class functions, where we treat functions just as we treat data: passing them to and returning them from other functions, and storing them to call later.

Callbacks make code more ready for change by allowing clients to provide their own code to run when an event occurs, so that the behavior doesn’t have to be hard-coded into the implementation beforehand.

Writing a single giant input loop to handle every possible event in a large system is neither safe from bugs nor easy to understand: that single piece of code becomes an all-knowing all-controlling behemoth. Callbacks allow each module in the system to be responsible for their own events.

In future readings, we’ll see further benefits to all three of these properties of good software from the idea of first-class functions.