Abstraction and Composition

Programming is essentially the art of composing expressions together into larger expressions, and then abstracting them into a function that can then be reused, with different parameters generalising the function’s behaviour. These two laws underpin all our programming: without composition we would be unable to build larger programmes, and without abstraction we would be unable to build upon what is already built.

However, I think many modern programming languages actually break these laws, rendering certain elements of the language uncomposable or unabstractable. This undermines the core precepts of programming, leaving us with programmes that cannot be built or built upon. I will provide two examples, through Java (for no reason other than its being well-known).

When you’re designing a programming language or other system, make sure to check that your code doesn’t break the laws of abstraction and composition, or you may run into these kinds of problems yourself.

Composing Interfaces

Whilst classes are properly separated into package namespaces, within a class a method is identified only by its name. This, combined with interfaces, effectively undermines all of Java’s namespacing, breaking the law of composition: if we have two interfaces, which may be imported from two separate packages, it is possible that a class is unable to implement both. Interfaces are not in general compositional. Consider the following two interfaces:

interface Performer
{
    String getName();
    void bow();
}

interface StringInstrument
{
    void pluck();
    void bow();
}

We cannot possibly implement both, because they each contain a method of the same name. This prevents us from implementing any kind of anthropomorphic string instrument, simply because to bow and to bow are spelt the same, even though they’re unrelated and even pronounced differently. Composition has failed!

Methods should instead be namespaced as well as classes:

class VioletaTheViolin implements Performer, StringInstrument
{
    String getName()
    {
        return "Violeta";
    }

    void Performer.bow()
    {
        // take a bow
    }

    void pluck()
    {
        // pluck own strings
    }

    void StringInstrument.bow()
    {
        // play self with bow
    }
}

We can then run a particular interface’s method by using a qualified method name. For example, we might call violeta.(StringInstrument.bow)(). This then restores composability.

Abstracting Exceptions

The common try–catch paradigm for exception handling may seem neat and relatively simple, but it actually breaks the law of abstraction, leading to code duplication. Suppose we’re writing a library for parsing log files. We have a parseEntry method, which takes a single line and returns an Entry, and we want to add a parseLog method that takes a stream and returns a whole list of entries.

List<Entry> parseLog(BufferedReader in) throws ParseException
{
    List<Entry> list = new ArrayList<>();
    String line;

    while((line = in.readLine()) != null) {
        list.add(parseEntry(line))
    }
    return list;
}

But parseEntry could throw a ParseException, which would unwind the call stack back up to the nearest catch block, aborting parseLog. What if we want to parse a log and continue despite any errors, and, say, (meta-)log them instead? We’d need to write a whole new method for that, which duplicates code. Abstraction has failed!

List<Entry> parseLogForce(BufferedReader in, Logger logger)
{
    List<Entry> list = new ArrayList<>();
    String line;

    while((line = in.readLine()) != null) {
        try {
            list.add(parseEntry(line))
        }
        catch(ParseException e) {
            logger.logException(e);
        }
    }
    return list;
}

Instead we should be able to choose whether we continue or not, when we call the function. We can do this using an alternative paradigm: condition handling. With condition handling we separate the try–catch mechanism into two parts, try–recover and do–handle:

List<Entry> parseLog(BufferedReader in) raises ParseException resumes SkipEntry
{
    List<Entry> list = new ArrayList<>();
    String line;

    while((line = in.readLine()) != null) {
        try {
            list.add(parseEntry(line))
        }
        recover(SkipEntry r) {
            continue;
        }
    }
    return list;
}

List<Entry> parseLogForce(BufferedReader in, Logger logger)
{
    do {
        return parseLog(in);
    }
    handle(ParseError e) {
        logger.logException(e);
        resume new SkipEntry();
    }
}

See my other article, Condition Handling for Non-Lispers, for more information (in Python).

Both of these changes would require significant changes to a language, but I do believe restoring the two laws of abstraction and composition, wherever they’ve been broken, would be worth it.