Wednesday, August 10, 2005

Computer Interfaces IV

(Fourth part of a series inspired by experiences in amateur tech support and the endless stream of articles yapping about how the time for desktop Linux is either never or next year.)

First part: On people and machines
Second part: On dumb machines
Third part: And the people who program them

Last time, I said that I'd show you a real interface. well here it is, written in C *. Please don't run away now! Everything inside /* and */ marks are comments, human-readable, and ignored by the compiler.

And, finally, here's the demonstration user interface (skip the code):

/* A really simple interface. The computer will begin running the code at main() */
#include <stdio.h>
#include <ctype.h>
#include <string.h>

/* show the introductory/help page */
void
show_help() {
printf("Simple interface example (pocket calculator)\n"
"Performs an operation on numbers a and b.\n"
"Enter numbers or operations when prompted,\n"
"Valid operations are:\n"
"+ Addition\n"
"- Subtraction\n"
"* Multiplication\n"
"/ Division\n"
"Type \'h\' for this help message or \'q\' to quit at any prompt\n"
"Press ENTER after your response.\n");
}

/* Prompt the user for a number, return that number.
If the user presses q, set do_quit = 1.
Do nothing if do_quit is already nonzero (the user already selected to quit) */
double
prompt_number(char which_number, int *do_quit) {
char response[20];
double number;
int i;
int do_over = 0;

if (!*do_quit) {
do {
do_over = 0;
printf("Number %c? ", which_number);
gets(response);
for (i = 0; isblank(response[i]); i++);
switch (response[i]) {
case 'q':
*do_quit = 1;
number = 0;
break;
case 'h':
show_help();
do_over = 1;
break;
default:
sscanf(response, "%lf", &number);
}
} while (do_over);
}
return number;
}

/* Ask what operation to do and return the answer.
If the user chooses to quit, set do_quit = 1.
Do nothing if do_quit is already nonzero (the user already selected to quit) */
double
operations_math(double a, double b, int *do_quit) {
char response[20];
int i, return_value;
int do_over;
double answer;

if (!*do_quit)
do {
do_over = 0;
printf("Operation (+ - * / q h)? ");
fgets(response, 20, stdin);
for (i=0; isblank(response[i]) && (i < strlen(response)); i++);
switch(response[i]) {
case '+':
answer = a + b;
break;
case '-':
answer = a - b;
break;
case '*':
answer = a * b;
break;
case '/':
if (b == 0.0) {
printf("ERROR! Attempt to divide by zero!\n");
answer = 0.0;
}
else
answer = a / b;
break;
case 'q':
answer = 0;
*do_quit = 1;
break;
default:
show_help();
do_over = 1;
}
} while (do_over);
return answer;
}

/* The program starts running here */
int
main() {
int should_quit = 0;
double a, b, ans;

show_help();

do {
a = prompt_number('a', &should_quit);
b = prompt_number('b', &should_quit);
ans = operations_math(a, b, &should_quit);
if (!should_quit)
printf("Answer: %f\n", ans);
} while (!should_quit);
return 0;
}

(* A high level computer language; operating system, C compiler, and standard C libraries required)

It's a few very short lines of code, but, it includes most of the elements of a “real” modern user interface:
First, it simplifies the complexities of the computer. A typical user session might look like this:

Simple interface example (pocket calculator)
Performs an operation on numbers a and b.
Enter numbers or operations when prompted,
Valid operations are:
+ Addition
- Subtraction
* Multiplication
/ Division
Type 'h' for this help message or 'q' to quit at any prompt
Press ENTER after your response.
Number a? 1
Number b? 2
Operation (+ - * / q h)? +
Answer: 3.000000
Number a? 8
Number b? 90
Operation (+ - * / q h)? /
Answer: 0.088889
Number a? 10
Number b? 0
Operation (+ - * / q h)? /
ERROR! Attempt to divide by zero!
Answer: 0.000000
Number a? q

In fact, this particular program simplifies so much, that it turns your thousand-dollar computer into a pocket calculator. Even though this is reduction to absurdity, it is being used to demonstrate, by example, that user interfaces limit what the user can do to what the programmer intended. In some way, the user is faced with the same trade-off as a programmer: facility versus power. This will hopefully become more evident in a future part of this series, when we look at some common user interfaces, and how that tradeoff works.

Second, it has a help facility and error messages. Without the help facility, if a user were to just sit down with this program, he/she wouldn't have a clue what to do with it. As in: “what's a ‘Number a?’ and what's it going to do for me?” Error messages are also quite important. They tell you one of two things: (1) you did something wrong. (2) the computer did something wrong. In my experience, users have become so click-happy that they tend to just go right through the error messages or pop-up boxes that are thrown at them without a second thought. Sitting down a few seconds with an error message, and trying to understand what it means is a first step to learning to how to deal without panicking when things go bump in the night. Sometimes, your guess will be wrong.** But, it is the way to computer zen. In fact, the lack of appropriate error messages in a user interface is in itself problematic. Which is a perfect segue into element number three...

It's got bugs! (Did you see them?) This program, in fact, has about the level of error a programmer might make after only one week in a programming course. Not all of the bugs were in code that's strictly user interface code, but, I think it's illustrative. Surprisingly, similar errors form the majority of security holes in modern computer programs. In no particular order:
Lack of error messages and error checking:
The only error condition that is caught here is division by zero. But, it's not the only possible error that can happen. Each declaration like this:
double answer;
reserves a certain amount of temporary memory for a named variable. In this case, we're declaring that the variable we call “answer” holds a “double precision floating point” value. It also reserves a certain amount of memory space for it. But, let's say that we try to add 1x10309 (which can be represented by 1e309) to any number. The answer will be infinity. The double precision floating point format simply can't support any number above 1x10308. There is no indication of an overflow or underflow condition anywhere in this code.

Even worse, if you type a letter where it asks for a number, there will be no indication that you typed incorrectly. The program will output an answer, but the answer would be garbage! Next time you see an error message, just think of what worse things might have happened if the error weren't reported back to you. Only the operations_menu() function correctly checks if the user typed a response to the question that the computer was actually asking.
Bad assumptions:
Notice these lines of code:

if (b == 0.0) {
printf("ERROR! Attempt to divide by zero!\n");
answer = 0.0;
}

You would think that they would catch all times that the variable b, which would otherwise be used in division, is equal to zero. In fact, it won't. This is because of some quirks in how the double precision floating point numbers are stored internally in the computer. There is sometimes some inherent round-off in numbers that will make numbers close to zero effectively equivalent to zero. A better comparison would choose some arbitrarily small (but representable) number epsilon and compare if (fabs(b) < epsilon), where fabs(x) is the absolute value of the floating point value x.
Buffer overruns:
Let's say I give you one sheet of paper to write on (and a pen) and a desk to sit at. I then bring a second person in the room who will dictate some text to you, and you write down whatever he said on your paper. The second person doesn't know that I only gave you one sheet of paper, so, he starts dictating the entire play Hamlet. At some point, you run out of space on the paper. One option for you is to stop dictation. Maybe you'd get another blank piece of paper. You could also start writing on the desk, and/or on any sheet of paper that happens to be sitting around, used or unused. Well, these two lines of code ask the user for a number and put the user's response into a variable helpfully called “response”:

printf("Number %c? ", which_number);
gets(response);

The variable was declared here
char response[20];
to be 20 characters long. What do you suppose happens when I type in more than 20 characters? Here, the computer will do the equivalent of writing on the desk. It will overwrite whatever happens to be in memory after the space I reserved for the response. If it contains something important, it'll be gone. The next line finds the first nonblank character in the response (that way, if someone types <space><space><enter>, it will still know to quit.
for (i = 0; isblank(response[i]); i++);
Again, it doesn't have any bounds checking. If the whole string contains blanks, it will stop at some random point in memory. Notice how differently these lines are implemented in operations_menu():
printf("Operation (+ - * / q h)? ");
fgets(response, 20, stdin);
for (i=0; isblank(response[i]) && (i < strlen(response)); i++);

The code asks the question, waits for a response which is limited to 20 characters, and finds the first nonblank character only if it exists within the memory reserved to hold the response. Buffer overruns are particularly dangerous, because the same memory that holds variables that the user can change also holds variables that are internal to the program, and the locations in memory that the computer may later execute as code. Bad input to a program that results in a buffer overrun is a common vector for the spread of computer viruses and worms. A Google search for “buffer overrun” reveals how many times this type of error happens in real programs (and how dangerous it can be).

It is likely that any sufficiently complex computer program has bugs. Bugs usually come about from the programmer not accounting for all possible inputs, and from logical errors. Sometimes, as is the case here, the logical errors can be subtle, and, even when testing the program, it will work. They usually aren't quite as trivial as the ones presented here. Bugs are a side effect of complexity.

Notice that the computer, at every time, would do exactly what it was told. The machine would be stupid enough to shoot itself in the foot if it were told to do so (provided that it had an external gun interface and a foot). Just something to keep in mind.

So, this time, I showed a simple example of how a user interface serves to limit what a user can do, and went into a tangential discussion about how bugs come about. Next time, we'll apply (hopefully, only the first of) those principles to a common user interface, and show just how the user interface will not only change the way you interact with the machine, but will change the way you think about what you can do with the machine in addition.

---
** A long, long time ago, in the dark ages of DOS, I got an "out of memory" message and thought that if I waited a few minutes and tried again, maybe the computer would somehow get its memory back. This makes no sense whatsoever, but, strangely enough, it worked. Go figure.

Technorati tags: , ,

Comments:
A lot of what you said applies not just to user interfaces, but to programmatic interfaces too. An API shapes what a programer (the "user" in this scenario) can do or express. Furthermore, an API can be well designed to limit the mistakes made with it. Look at the C stdlib string APIs and shudder. Even strncpy is constantly used wrong because it's so hard to get right. Compare that to djb's string libraries and you'll see why his code is so stable. Overruns, error handling and assumptions are every bit as important in an API and they are in a UI.
 
Agreed.
UI:User::API/High level library:Programmer.

One of the major points I'm trying to sneak into this series is that every user is in some way equivalent to a programmer (as much as someone's UI metaphor tries to hide it) but, I'm getting ahead of myself.

I'm picking on UI's because they are the way most people think of their computers. And, not in the sense of "this is what someoene is presenting to me," but, in the sense of "this is what my machine is." I guess the point of this part, from a user perspective, was to think of error messages as (1) generally good things, (2) possibly correctable things, and (3) not as black boxes.

(That, and writing intentionally bad, and yet, still really simple code was kind of fun :-) )
 
"I guess the point of this part, from a user perspective, was to think of error messages as (1) generally good things"

I disagree. Error messages are a failure on the programmers part. It's much harder, but the programmer should have made the system so that the error couldn't have happened. Most programmers rebel at that thought, but a system should be designed so that the user cannot do the impossible, prompting an error message. For every error message in your code, rethink how to make it so that condition can never happen.

Read some books by Alan Cooper for more on this design philosphy. It's not perfect (a program can't anticipate everything) but it's a excellent design principle. I've found that it has changed the way I approach a program, even if I still have some error messages.

BTW, in case people are trying to guess my profession, I am not a professional programmer.
 
Sometimes, it's about a choice of which type of UI widget to use. Like, choosing a radio button or drop-down list box instead of an edit box. In those cases, if the programmer used a control that allowed bad input, it's a sign of bad programming.

There are times when it's possible to exclude the impossible, and there are times when it isn't. Hardware-related errors usually fall in that category. (The alternatives being to keep going as if nothing happened or bum out without a word).

Also, anything you do at a command line interface has to assume that the user could have typed absolutely anything.

And, any file loading function in an end-user product should assume that the incoming data may not be in the right format. Sometimes, it's possible to work around the format issues, sometimes it isn't. Personally, I'd rather know that the program tried to work around something than just have it read in a file as garbage. Isn't that what stderr (the "standard error device") is for?

Maybe I'll do a post on collected screen shots of error messages.
 
"Sometimes, it's about a choice of which type of UI widget to use."

It goes deeper than that. For example, how many times have you menued "File|Save As", chosen a directory, typed a good filename, clicked ok, only to get an annoying error message because you don't have write privileges to that directory? Not only is it aggravating, but it loses the work you put into typing a filename! There's no reason a program can't avoid offering directories that are read-only in a save dialog. It just takes more and better programming.

"Hardware-related errors usually fall in that category."

Even hardware errors can be handled better. Journaled filesystems do not yoke the user will onerous fscks on bootup, but are much harder to write. Browser can offer to go into offline mode uin reposne to anetwork outage instead of displaynig a cryptic error message stinking of finality.

"Also, anything you do at a command line interface has to assume that the user could have typed absolutely anything."

Even CLIs can improve. Look at tab completion. A great idea that reduces the number of error messages by filling in the correct program or filename without typos. Programmatic completion goes one better by being context aware. Typing "ping sa" and hitting tab will offer to complete "sa" with all the entried in your hosts file that start with sa. It can even know what command line parameters a program will accept. Or try a Juniper router. Their cli is always offering to be helpful, severely limiting the number of error message you can get. You *can't* mistype an interface name; it won't let you.

"And, any file loading function in an end-user product should assume that the incoming data may not be in the right format."

A slightly different topic, but still crucial. All data must be assumed to be tainted and/or corrupted. This is almost always ignored; try fuzzing any file with some random corruption and watch as programs segfault when opening it. This problem is getting more attention now that this class of vulnerability is being actively exploited by attackers.
 
Even CLIs can improve. Look at tab completion. A great idea that reduces the number of error messages by filling in the correct program or filename without typos

Agreed, CLI's aren't immune to improvement (is anything?). But, even the best completions wouldn't prevent me from writing a command like this:
$ bc -l /usr/bin/gcc

Some kind of "command prototyping" might do that, though.

Even hardware errors can be handled better. Journaled filesystems do not yoke the user will onerous fscks on bootup, but are much harder to write.

On the other hand, not every applications programmer is or wants to be a systems programmer, and, there is something to be said for standard high-level interfaces. So, someone writing a word processor has no control over the file system, but does have control over what the dialog box displays.

Browser can offer to go into offline mode uin reposne to anetwork outage instead of displaynig a cryptic error message stinking of finality.

Again, I'd want the program to tell me what it did, so, when I type "http://elfsdh.blogspot.com" in the location bar, I'll know why it doesn't work.

There's no reason a program can't avoid offering directories that are read-only in a save dialog. It just takes more and better programming.

I'm not sure I agree with you on not showing read-only directories. It makes the programming incredibly complicated (and, all additional complications mean that there are ever more perverse possibilities to consider). In your file|save as example, let's say I have a read-only directory with a write-capable subdirectory five levels down. Your solution would even prevent a normal user from navigating through / ! I could see having a "read-only" flag in the dialog box, and/or not allowing someone to hit the "OK" button. There does come a time when a programmer tries to make his program "too smart" and ends up making every user try to figure out what they were thinking. (I'm thinking of Clippy here, who can shove his "helpful" suggestions ... ).
 
"Agreed, CLI's aren't immune to improvement (is anything?)"

Me! :-)

"not every applications programmer is or wants to be a systems programmer"

Dream on! Kernel programmers get aaaaaall the chicks :-)

"Again, I'd want the program to tell me what it did, so, when I type "http://elfsdh.blogspot.com" in the location bar, I'll know why it doesn't work."

User feedback is crucial.

"n your file|save as example, let's say I have a read-only directory with a write-capable subdirectory five levels down."

That's failure of our filesystem design! Users almost NEVER make folders nested that deep. It's a programmers malaise to design for every possiblity, hurting the usage of the well-trafficked common case. Folders are metadata, and nesting is just confusing. Allow for other possiblities; design for the common.

Check out how BeOS used its filesystem (you have to use it to appreciate it). Most next-gen OSes like Longhorn and MacOSX are moving toward that model. In fact, the guy who wrote BeFS was hired by Apple to work on journaling HFS+, adding a database of live metadata, and Spotlight.

"There does come a time when a programmer tries to make his program "too smart" and ends up making every user try to figure out what they were thinking."

Read Alan Cooper. Really, you'll LOVE it.
 
You know what this conversation needs? Kefira. :-)
 
That's failure of our filesystem design! Users almost NEVER make folders nested that deep

You've obviously never met me. I am the filesystem's worst nightmare.

Anyway, let's say you want to save a file on a USB disk mounted at /media/usbdisk . You start at /home/mis-nagid , and you're too bloody lazy to type /media/usbdisk (or, your dialog box only allows you to use the navigator because otherwise the user might misspell a directory name), so you try to navigate the dialog...

Back up to /home, which is not writable to users, so, it's impossible to get to the disk without some work with symlinks that would have been unnecessary if you could just navigate the whole filesystem.

Allow for other possiblities; design for the common.

I think the key here is not to box in users too much. And, at some point, you gotta work with what you've got (or be content with the release schedule of the HURD).

You know what this conversation needs? Kefira. :-)

I wish I could oblige...
 
Post a Comment

<< Home