Unix Philosophy Enforces Simplicity

Unix Philosophy Enforces Simplicity
Photo by Tim Wright

In R&D of the Pharmaceutical industry there is a strong culture of reviewing - everything. This isn't without reason. Many of the documents produced ends up being used to decide how patients should be treated. The material used for such decisions has to be sound.

At Novo Nordisk I have, together with Edita Karosiene, been working on making a program that can be used for documenting review on Unix based systems. We call it Trace.

Trace

The idea is this: Once you complete a program, you sign it. Once you review a program, you sign it. When a file is modified its signatures are lost. 0 signatures = "Idle", 1 signature = "Owned", +2 signatures = "Reviewed". That's it.

Users interface with Trace via the Unix command line, using Trace's 2 commands: trace and sign. To sign a file, simply:

>sign file1 
file1 is Owned 

To see the status of all files in a directory:

>trace . 
reviewed jejt edka file1 
idle               file2 
owned    tkqt      file3 
>

To add a file to Trace:

>trace newFile 
newFile is not added to Trace, do you want to add it? (y/n) 
>y newFile is added to Trace. 
>

Sign a file that is not added to Trace:

>sign newFile 
newFile2 is not added to Trace, do you want to add and sign it? (y/n) 
>y 
newFile is added to Trace. 
newFile is Owned 
>

We recently finished writing the first implementation of Trace, and we were excited! However, the excitement was quickly replaced by an all too familiar stomach feeling.

Something Didn't Feel Right

After having played a bit around with Trace, signing some files and printing some statuses it became clear that Trace felt out of place. While it did live on the Unix command line, it didn't fell at home with its famous friends: rm, cd, ls, cat, ps, ...

After pondering for too long about what was wrong, I did what I do too rarely. I read a book - The Art of Unix Programming by Eric S. Raymond.

Already in the first chapter the book confirmed our gut feeling.

The section Basics of the Unix Philosophy summaries different attempts of boiling down the Unix design principles into something quotable. The most famous attempt can be attributed to Doug McIlroy.

This is the Unix philosophy: Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface. --Doug McIlroy

While telling you what to achieve, the quote does not tell you how to achieve it. Luckily the author also has his own, more verbose, take on the philosophy, expressed through 17 rules.

Shhh!

Reading the 17 rules made it clear that we were outlaws.

Rule of Silence: When a program has nothing surprising to say, it should say nothing.

Trace was not silent. At all. Every command followed up with a message confirming what had just happened. Trace had 21 different messages to print, 3 of which were prompts that the user had to answer.

Confirming that this was a problem was easy, but figuring out how to solve it, a lot harder. An obvious solution would have been to remove the prompts and instead assume that the answer was always "yes". However, this would potentially make Trace do things the user did not expect or, even worse, did not intend. Both would be violations of another rule.

Rule of Least Surprise: In interface design, always do the least surprising thing.

Eventually we realised our problem. While we with Trace are attempting to do one thing well, the individual commands did not. They tried to do too much.

The Wrong Kind of Simple

The idea of compressing all functionality into 2 commands was to make Trace as simple as possible. But we had gone too far. All functionality was crammed together, which forced the use of guidance prompts to navigate the user through all the possible paths of a command.

The strive to simplify the interface had made it accidentally complex.

It's Not Rocket Science

The breakthrough in finding the solution came, as it is so often the case, by explaining the problem to a college. While discussing it I ended up drawing a diagram showing how Traces commands were connected to its possible actions:

No alt text provided for this image

Drawing it made the solution embarrassingly obvious: Make a command for each action (do one thing and do it well).

No alt text provided for this image

ARG#HH!!@$!

Returning to our example, what should happen when a user tries to sign a file that isn't added to Trace? It cannot add the file, because that is add’s job. Another rule gave the answer:

Rule of Repair: When you must fail, fail noisily and as soon as possible.

If a command is asked to do what it cannot, it should fail.

>trace sign newFile 
cannot sign: newFile isn't added to Trace. 
>

Add it. Sign it. Print status.

>trace add newFile 
>trace sign newFile 
>trace status
reviewed jejt edka file1.txt
idle               file2.txt
owned    tkqt      file3.txt
owned    jejt      newFile 
>

add and sign can now obey the Rule of Silence because they follow the Rule of Least Surprise.

21 to 7

When rewriting the implementation to fit our redefined interface, it became even clearer why the "Unix philosophers" seem to be somewhat obsessive about it. The code became much simpler. All the branching logic necessary to encode the different paths of each command, could be removed and replaced by a much simpler implementation: Check if the command is allowed to run, if not: print an error, else: run the command.

The rewrite reduced 21 messages to 7 and the 3 prompts had completely disappeared. Trace now fells much more at home.

One of my most productive days was throwing away 1,000 lines of code. -- Ken Thompson