Some Interface Examples

In my previous post on interface design principles, I sort of assumed that people would know what I meant by “primary interface definition”. In Security As A Class Of Interface Guarantee I defined it it as follows:

The primary interface definition is the immediately accessible surface of the interface itself, e.g. a function or method declaration, an IDL specification or other code generation/specification system for network protocols, the grammar of a programming language, or a user-facing GUI or CLI. A secondary interface definition is supplementary material; usually documentation, annotation, post-facto errata, entries in issue trackers, commit log messages, et c.

Our ideal is, or should be, to make interfaces so simple to use that people can learn them immediately, and use them readily. Additionally, it should be impossible (or at least difficult) to use the interface unsafely. (“Unsafety” could mean whatever you like: deleting important information, not deleting information that the person needs deleted, sending information to the wrong person, not sending information to the right person, crashing the machine, and so on.)

A learnable interface explains itself upon contact — the person using it has no need of secondary interface definitions such as documentation. Like a screwdriver, the shape of the head either fits or does not fit the screw. If the person somehow manages to crank the wrong screwdriver into a screw head, the damage will be immediate, obvious, and commensurate with how hard the person cranked it. (Unlike so much software that explodes silently, a week later...)

Picture of screwdrivers and their matching
screw heads.
From Universal Screwdriver. Maybe we can make software interfaces more universal.

I also assumed people would know what it means for an interface to be as simple as possible, but no simpler. For example, maybe the Universal Screwdriver really works — but maybe it doesn’t. It seems unlikely to perfectly fit the different types of screw heads. It’s worth a try, but we should fall back to regular screwdrivers if necessary.

Here are 2 more examples. I am a fan of dialog boxes that really offer choice, rather than the often meaningless OK and Cancel ‘non-dialog’ boxes. Sometimes, those 2 choices are just not enough to give the person what they need.

Screenshot of 3 choices when deleting a
recurring calendar event: Only This Instance, All Following, and All Events In
The Series.
Screenshot of 3 choices when deleting a recurring calendar event: Only This Instance, All Following, and All Events In The Series.
Screenshot of 3 choices when exiting a
text editor: Close Without Saving, Cancel, and Save As.
Screenshot of 3 choices when exiting a text editor: Close Without Saving, Cancel, and Save As.

The only way to simplify the calendar example would be to over-simplify it: to remove meaningful choices. The safest choice, Only This Instance, is first, followed by the still-pretty-safe All Following. But All Events In The Series is sometimes necessary. And of course, Google Calendar does offer Undo.

The text editor example could be simplified; for example, Mac OS X TextEdit.app and Google Docs both constantly auto-save. They entirely get rid of the concept of saving, which is great because people throughout the Cyber Age have lost tons of work because they forgot to save. All that remains of the old Save/Save As... paradigm is the still-meaningful Export..., Rename..., and Make A Copy....

Note that auto-saving applications must either automatically name files (Mac OS X; see “The Document Architecture Provides Many Capabilities for Free”), or do away with the concept of the filesystem by replacing it with search (Google Drive).

One could argue, as I probably would, that a programmer’s or engineer’s text editor still needs a concept of explicit commit, and hence should not auto-save or at least not auto-name. Therefore, such editors do still need to raise a dialog if there are un-committed buffers on exit; given that, Gedit’s 3-choice dialog seems to fit people’s needs well. On the other hand, one could argue that the revision control system (e.g. Git; ideally integrated into the editor as in most IDEs) exists to provide explicit commit, and that the editor could and hence should auto-save (possibly into a temporary, private branch in the RCS). But it probably still could not auto-name.

Thus, even in the case of the engineer’s text editor, no further simplification is possible. You can move the complexity around, creating 1 giant complex system (e.g. Eclipse) or integrating many small tools with glue code (e.g. bash + vim + git), but you can’t eliminate it without losing something crucial.

Although the examples so far have been of UIs, we can apply the same interface design principles to APIs. APIs are, after all, UIs for engineers. (The same is true of a programming language’s syntax, and the principles apply there, too.) Consider the get method of the Java HashMap class. From the documentation:

public V get(Object key)

Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.

More formally, if this map contains a mapping from a key k to a value v such that (key==null ? k==null : key.equals(k)), then this method returns v; otherwise it returns null. (There can be at most one such mapping.)

A return value of null does not necessarily indicate that the map contains no mapping for the key; it's also possible that the map explicitly maps the key to null. The containsKey operation may be used to distinguish these two cases.

Specified by:

get in interface Map<K,V>

Overrides:

get in class AbstractMap<K,V>

Parameters:

key - the key whose associated value is to be returned

Returns:

the value to which the specified key is mapped, or null if this map contains no mapping for the key

See Also:

put(Object, Object)

Look how verbose the documentation is. Part of the verbosity is ‘necessary’ to explain the interface’s unfortunate ambiguity (the meaning of a null return value), and part of the verbosity is gratuitous (the meaning of the key parameter, whose name and type make it obvious to a reader who has read and understood the Map interface).

Note also that the documentation as verbose as it is, does not explain the exceptions that the implementation might throw. The reader has to follow the links up the inheritance chain to find the exceptions.

Still, this interface does have the virtue of explaining itself immediately upon contact, somewhat like the screwdriver. Compare it to Python’s online help:

>>> help({}.__getitem__)
Help on built-in function __getitem__:

__getitem__(...)
    x.__getitem__(y) <==> x[y]

And of course, Python exceptions are all run-time exceptions; there is no equivalent of Java’s checked exceptions (which make up part of the declared and statically-checked type of an interface).

Between these extremes of annoying verbosity and useless terseness, there is the admirably concise but eye-rollingly gnomic Hindley-Milner type system notation.

It’s easy to imagine, and in C++ and Java easy to actually achieve, interface definitions that explain themselves concisely like Hindley-Milner, but without the gnomic Greek. For example, a better Map.get in Java:

public V get(K key) throws NoSuchKeyException

Or, if you dislike exceptions and like Maybe types as I do,

public MaybeV get(K key)

Either way, the interface explains itself in 1 concise line, without the ambiguity of null, with no need for verbosity, and with no need to crawl up the inheritance chain for more clues. Just as the labels in buttons like All Events In The Series and Close Without Saving explain themselves concisely, the names of types and identifiers explain themselves to readers who have understood the 1-paragraph definitions of Map and Maybe. Callers that violate the interface simply won’t compile, let alone run — no nasty run-time surprises like in Python.

(Note that in defining the key type K as the argument type of get, we avoid another ambiguity in the Java Map interface: the possibility of ClassCastException (documented as “optional”). Recall from the documentation above that Java’s Map.get takes Object as the argument type — which may not correctly cast, at run-time (!), to K.)

There is a whole rant to be written on another topic I touched on in the last post: Whether or not an interface’s guarantee is computable. But, it will have to wait; for now, it is time to compose these 2 interfaces:

class Person {
  // ...
  MaybeSatisfaction eat(Eatable* eatable) mutable;
  // ...
}

class ChocolateChipPancake : public Eatable {
  // ...
}