Captive User Interfaces — Why You Should Avoid Them

This tutorial shows you the meaning of captive user interfaces and why they’re discouraged under the Unix philosophy. I’ve written this as a first chapter draft for my upcoming book “From One to Zero” to appear in 2020 with San Francisco-based publisher NoStarch.

What’s a Captive User Interface (CUI)?

A captive user interface is a way of designing a program that requires the user to interact with the program in a session before they’ll be able to proceed with their main execution flow. If you invoke a program in your terminal (Windows, MacOS, or Linux), you must communicate with the program before you can go back to the terminal. Examples are mini programs such as SSH, top, cat, vim—as well as programming language features such as Python’s input() function.

Python Example Captive User Interface

Say you create a simple life expectancy calculator in Python. The user must type in their age and it returns the expected number of years left based on a straightforward heuristic. This is a fun project found here.

"If you’re under 85, your life expectancy is 72 minus 80% of your age.
Otherwise it’s 22 minus 20% of your age."

Your initial, bad, Python code is shown here:

def your_life_expectancy():
    age = int(input('how old are you? '))
    
    if age<85:
        exp_years = 72 - 0.8 * age
    else:
        exp_years = 22 - 0.2 * age

    print(f'People your age have on average {exp_years} years left - use them wisely!')


your_life_expectancy()

Listing 8-8: Life-expectancy calculator – a simple heuristic – implemented as a captive user interface.

Here are some runs of the code:

>>> how old are you? 10
People your age have on average 64.0 years left - use them wisely!
>>> how old are you? 20
People your age have on average 56.0 years left - use them wisely!
>>> how old are you? 77
People your age have on average 10.399999999999999 years left - use them wisely!

Interactive Jupyter Notebook to Calculate Your Life Expectancy Using a Captive User Interface

In case you want to try it yourself, I’ve created an interactive Jupyter notebook you can run in your browser to calculate your own life expectancy. But, please, don’t take it too serious! Here’s the notebook:

The code makes use of Python’s input() function that blocks the program execution and waits for user input. Without user input, the code doesn’t do anything. This seriously limits the usability of the code.

What if I wanted to calculate the life expectancy for every age from 1 to 100 based on the heuristic and plot it? I’d have to manually type 100 different ages and store the results in a separate file. Then, you’d have to copy&paste the results into a new script to plot it.

The function really does two things: process the user input and calculate the life expectancy. This already violates rule number 3: Make Every Program Do One Thing Well.

But it also violates our rule: don’t use captive user interfaces if possible.

Non-Captive User Interface Python Example

Here’s how the function could’ve been implemented more cleanly:

def your_life_expectancy(age):
    if age<85:
        return 72 - 0.8 * age
    return 22 - 0.2 * age


age = int(input('how old are you? '))
exp_years = your_life_expectancy(age)
print(f'People your age have on average {exp_years} years left - use them wisely!')

Listing: Life-expectancy calculator – a simple heuristic – without captive user interface.

The code is functionally identical to the code with captive user interface. However, it has a big advantage: now, you can use the function in different and unexpected—by the initial developer—ways:

import matplotlib.pyplot as plt


def your_life_expectancy(age):
    '''Returns the expected remaining number of years.'''
    if age<85:
        return 72 - 0.8 * age
    return 22 - 0.2 * age


# Plot for first 100 years
plt.plot(range(100), [your_life_expectancy(i) for i in range(100)])

# Style plot
plt.xlabel('Age')
plt.ylabel('No. Years Left')
plt.grid()

# Show and save plot
plt.savefig('age_plot.jpg')
plt.savefig('age_plot.pdf')
plt.show()

Listing: Code to plot the life expectancy for years 0-99.

The resulting plot is shown in the following figure:

Figure: How the heuristic works for input years 0-99.

Let’s not talk too much about the flaws of this heuristic—it’s crude by design—but focus on how the rule of avoiding captive user interface has helped us produce this plot. Without the rule, we’d have to write a new function, add redundancies and unnecessary complexity. By considering the rule, we’ve simplified the code and opened up all kinds of future programs to use and built-upon the heuristic. Instead of optimizing for one specific use case, we’ve written the code in a general way that can be used by hundreds of different applications.