6.031TS
6.031TS — Software Construction
TypeScript Pilot — Spring 2021

Reading 25: 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 listeners, callback functions that are called when an event happens. We discuss this idea in the context of graphical user interfaces, where listeners are used to respond to incoming input events.

Listeners and callbacks are an example of a bigger idea, first-class functions, or treating functions just like data: passing them as parameters, returning them as results, and storing them in variables and data structures. We’ll see more uses for first-class functions over the next few classes.

Input handling in a graphical user interface

Input is handled differently in graphical user interfaces (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 email client were written that way, it might look like this (in pseudocode):

while (true) {
    read mouse click
    if (clicked on Check Mail button) doRefreshInbox();
    else if (clicked on Compose button) doStartNewEmailMessage();
    else if (clicked on an email in the inbox) doOpenEmail(...);
    ...
}

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. For example, here is how you create a button (in various languages):

Java:

JButton playButton = new JButton("Play");

HTML:

<button id='playButton'>Play</button>

TypeScript (accompanying the HTML):

const playButton = document.getElementById('playButton');

The resulting button looks on screen something like this:

This button handles its own input from the mouse and keyboard. 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 your program do something when the button is clicked, you attach a listener to it:

Java:

playButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        playSound();
    } 
});

TypeScript:

playButton.addEventListener('click', function(event:MouseEvent):void {
  playSound();
});

GUI event handling is an instance of the Listener pattern, also known as Publish-Subscribe. In the Listener pattern:

  • An event source generates (or publishes) a stream of discrete events, which correspond to state transitions in the source.
  • One or more listeners register interest (subscribe) 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 – in Java, the anonymous ActionListener instance, and in TypeScript, the function expression;
  • the function called when the event happens is actionPerformed.

An event often includes additional information which might be bundled into an event object (like the ActionEvent 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:

  • A top-level event loop reads input from mouse and keyboard. In Java and most graphical user interface toolkits, this loop is actually hidden from you. It’s buried inside the toolkit, often running on a separate thread created by the toolkit, and listeners appear to be called magically.
  • Each listener does its thing (which might involve e.g. modifying objects in the view tree), 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 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. For example, in Java:

  • JButton sends an action event when it is pressed (whether by mouse or keyboard)
  • JList sends a selection event when the selected element changes (whether by mouse or by keyboard)
  • JTextField sends change events when the text inside it changes for any reason

And in HTML:

  • <button> sends a click event after it receives both a mousedown (the mouse was pressed down) and a mouseup event (when the mouse was released).
  • <select> sends a change event when the user selects a different element from the dropdown menu (whether by mouse or by keyboard)
  • <input type=text> sends change events when the text inside it changes

reading exercises

Listeners

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

launchButton = new JButton("Launch the Missiles");

launchButton.addActionListener(launchMissiles);

launchMissilesactionPerformed() method is called

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

(missing explanation)

Listener vs. Runnable

The code used above to create an ActionListener for a button:

button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        ...
    } 
});

… should remind you of the way we have created the Runnable of a thread, also using an anonymous class:

new Thread(new Runnable() {
    public void run() {
        ... 
    }
}).start();

How is a listener similar to a runnable, and how are they different?

(missing explanation)

(missing explanation)

(missing explanation)

Don’t call me, I’ll call you

The actionPerformed 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:

playButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        playSound();
    } 
});

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)

HttpHandler vs. ActionListener

Compare and contrast the code for registering an Express route handler:

app.get('/song', function songHandler(request: Request, response: Response):void {
   ...
});

with the earlier code for registering a listener for a button:

playButton.addEventListener('click', function clickHandler(event:MouseEvent):void {
  playSound();
});

(missing explanation)

(missing explanation)

(missing explanation)

First-class functions

Using callbacks requires a programming language in which functions are first-class, which means they can be treated like any other value in the language: passed as parameters, returned as return values, and stored in variables and data structures.

Programming languages are full of things that are not first-class. For example, access control is not first-class – you can’t pass public or private as a parameter into a function, or store it in a data structure. The notion of public and private is an aspect of your program that Java offers no way to refer to or manipulate at runtime. Similarly, a while loop or if statement is not first-class. You can’t refer to that piece of code all by itself, or manipulate it at runtime.

In old programming languages, only data was first-class: built-in types (like numbers) and user-defined types. But in modern programming languages, like Python and JavaScript, both data and functions are first-class. First-class functions are a very powerful programming idea. The first practical programming language that used them was Lisp, invented by John McCarthy at MIT. But the idea of programming with functions as first-class values actually predates computers, tracing back to Alonzo Church’s lambda calculus. The lambda calculus used the Greek letter λ to define new functions; this term stuck, and you’ll see it as a keyword not only in Lisp and its descendants, but also in Python.

Guido van Rossum, the creator of Python, wrote a blog post about the design principle that led not only to first-class functions in Python, but first-class methods as well: First-class Everything.

In Java, the only first-class values are primitive values (ints, booleans, characters, etc.) and object references. But objects can carry functions with them, in the form of methods. So it turns out that the way to implement a first-class function, in an object-oriented programming language like Java that doesn’t support first-class functions directly, is to use an object with a method representing the function.

We’ve actually seen this before in the context of Java threads: the Runnable object that you pass to a Thread constructor is a first-class function, void run().

This design pattern is called a functional object, an object whose purpose is to represent a function. The spec for a functional object in Java is given by an interface, called a Single Abstract Method (SAM) interface because it contains just one method.

Lambda expressions in Java

Java’s lambda expression syntax provides a succinct way to create instances of functional objects. For example, instead of writing:

new Thread(new Runnable() {
    public void run() {
        System.out.println("Hello!");
    }
}).start();

we can use a lambda expression:

new Thread(() -> {
    System.out.println("Hello!");
}).start();

There’s no magic here: Java still doesn’t have first-class functions. So you can only use a lambda when the Java compiler can verify two things:

  1. It must be able to determine the type of the functional object the lambda will create. In this example, the compiler sees that the Thread constructor takes a Runnable, so it will infer that the type must be Runnable.
  2. This inferred type must be a functional interface: an interface with only one (abstract) method. In this example, Runnable indeed only has a single method — void run() — so the compiler knows the code in the body of the lambda belongs in the body of a run method of a new Runnable object.

Lambda expressions in TypeScript

TypeScript/JavaScript, by contrast, does have first-class functions. A function is a first-class type, alongside objects and primitives.

We have already been using function expressions, starting as far back as Testing, since the mocha framework uses them:

describe("Math.max", function() {
  it("covers a < b", function() {
    assert.strictEqual(Math.max(1, 2), 2);
  });
});

Like Java, TypeScript has a more compact arrow syntax that avoids the need for a function keyword. A key difference is that the arrow in TypeScript looks like =>:

describe("Math.max", () => {
  it("covers a < b", () => {
    assert.strictEqual(Math.max(1, 2), 2);
  });
});

If the body of the function consists of only a single expression (i.e. like a Python lambda expression), then even the curly braces can be omitted:

it("covers a < b", () => assert.strictEqual(Math.max(1, 2), 2) );

Arrow functions can also be async, just like function expressions can:

it("should play the right song", async () => {
  const response = await fetch('http://localhost:8000/song/0');
  ...
});

But it turns out that there is a technical difference beween arrow functions and function expressions that is important when using methods: a function expression may redefine this, but an arrow function uses the definition of this from its surrouding context. So arrow functions should always be used inside an instance method, rather than function expressions. Understanding This, Bind, Call, and Apply in JavaScript has a good explanation of the issues behind the meaning of this in JavaScript.

reading exercises

Comparator<‌Dog>

In Java, suppose we have:

public interface Dog {
    public String name();
    public Breed breed();
    public int loudnessOfBark();
}

We have several Dog objects, and we’d like to keep a collection of them, sorted by how loud they bark.

Java provides an interface SortedSet for sorted sets of objects. TreeSet implements SortedSet, so we’ll use that.

The TreeSet constructor takes as an argument a Comparator that tells it how to compare two objects in the set; in this case, two Dogs.

Comparator is a functional interface: it has a single unimplemented method: int compare(...).

So our code will look like:

SortedSet<Dog> dogsQuietToLoud = new TreeSet<>(... /* a Comparator<Dog> that sorts by loudness */);
dogsQuietToLoud.add(...);
dogsQuietToLoud.add(...);
dogsQuietToLoud.add(...);
// ...

An instance of Comparator is an example of:

(missing explanation)

Barking up the wrong TreeSet

Which of these would create a TreeSet to sort our dogs from quietest bark to loudest?

Read the documentation for Comparator.compare(...) to understand what it needs to do.

new TreeSet<>(new Comparator<Dog>() {
  public int compare(Dog dog1, Dog dog2) {
    return dog2.loudnessOfBark() - dog1.loudnessOfBark();
  }
});

(missing explanation)

new TreeSet<>(new Comparator<Dog>() {
  public Dog compare(Dog dog1, Dog dog2) {
    return dog1.loudnessOfBark() > dog2.loudnessOfBark() ? dog1 : dog2;
  }
});

(missing explanation)

new TreeSet<>((dog1, dog2) -> {
  return dog1.loudnessOfBark() - dog2.loudnessOfBark();
});

(missing explanation)

public class DogBarkComparator implements Comparator<Dog> {
  public int compare(Dog dog1, Dog dog2) {
    return dog1.loudnessOfBark() - dog2.loudnessOfBark();
  }
}
// ...
new TreeSet<>(new DogBarkComparator());

(missing explanation)

Listener lambda

Consider this anonymous class from earlier in the reading:

playButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        playSound();
    } 
});

Fill in the blanks to transform the anonymous class into a lambda expression:

playButton.addActionListener(▶▶A◀◀ -> ▶▶B◀◀);

(missing explanation)

Implementing an event source

All our examples of callbacks so far have been 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 callback functions, and make the calls? To illustrate this, we’ll implement a simple counter that calls its listeners whenever it increments.

Counter spec

Counter has several operations:

export class Counter {

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

    /** @return 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: NumberListener):void  { ... }

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

We also define NumberListener as an alias for the type of the listener function, which simply takes the number that the counter has reached:

type NumberListener = (numberReached:bigint) => void;

Be careful here: in this context => is a function type, not a function expression.

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

const counter = new Counter();
counter.addEventListener(function(value:bigint) {
    console.log(value);
});

or, using arrow syntax:

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<NumberListener> = [];


// 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: NumberListener):void {
    this.listeners.push(listener);
}

public removeEventListener(listener: NumberListener):void {
    for (let i = 0; i < this.listeners.length; ++i) {
        if (this.listeners[i] === listener) {
            this.listeners.splice(i, 1);
            return;
        }
    }
}

private callListeners():void {
    for (const listener of 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’s numberReached() method.

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

reading exercises

A one-time listener

Suppose a client 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 is being steadily incremented in a background thread. Which of the following is most likely to happen when this listener is called?

(missing explanation)

Summary

Callbacks are our first 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.