Error Handling and Mathematical Closure

I Just Want Closure

Take e = a – b – c – d. It always returns a result. I never worry about whether it fails or if e is invalid. Can you imagine what it would be like if I had to worry about invalid results? I’d have to test the values, the values of sub-expressions, ensure that expressions are conditionally invoked… Suddenly, this simple formula becomes a multi-line headache. Yet, a long time ago, subtraction DID return invalid results, until mathematicians did something about it.

LONG ago, all numbers were positive whole numbers – even 0 did not exist. Then people tried doing things like 1-1. Since 0 wasn’t a number, 1-1 was undefined! Mathematicians took note, and instead of blaming the users, they fixed things They generalized subtraction so that 1-1, 2-2, etc… worked (and of course ensured all the old subtractions worked), and added 0 to the set of numbers.

The addition of 0 was tough; we take it for granted now, but think about it. 0 means no quantity, nothing. When you are counting, is it really intuitive that nothing is a number? Nothing is… well nothing! Yet 0 enabled a great many things, including the positional system that has served us so well. In fact, some consider it one of humanity’s great achievements!

Well time went on and people kept doing things with numbers that produced invalid results, and the mathematicians kept fixing things. First there were negative numbers, thanks to things like 1-2. Then there were fractions thanks to things like 1/2, and so on. At each step, mathematicians extended the number system by generalizing the operation so that new and old operations worked, and adding the necessary new numbers. In each case, they took care that all the relations of the number system still held.

Eventually they (mostly) achieved closure. No, I’m not talking about relationships, or even computer science. I’m talking mathematical closure which means an operation over things always produces the same kind of things. So for instance, to say the numbers are closed under addition, means that addition of numbers always produces a number. Dividing by 0 sadly would remain undefined.

 

Back to the Present

We face mathematical closure issues in programming too, although we may not recognize them as such. Let’s take this psuedo-code snippet:

class Test {
    Test merge(Test item) {
        Test result = null
        if ( this.is_compatible_with(item) ) {
            // result = results of merge
            ...
        }
        return result; // null if not compatible, to signal error
    }
    ...
}

Let’s say a,b,c,d and e are instances of Test, and I now want to merge a,b,c and d and store them in e. Well, it’s very much like the math equation above, so it’s tempting to write:

    e = a.merge(b).merge(c).merge(d)

But unfortunately there’s a problem; if the merge fails on any of a,b,c or d, this expression would result in an error because i’m trying to invoke a method on a null. Breaking up the expression and testing each part before going on is a pain in the neck, and exceptions introduce flow control issues. I want to have my cake and eat it too; I want error detection AND brevity.

My problem is that Test is not closed under merge, so I need to fix that. The way I do it is the way the mathematicians fixed their systems. I extend Test so it includes the “undefined” values. here’s one way:

class Test {
    Test() {
        this.set_valid(false);
    }

    Test merge(Test item) {
        Test result = new Test;
        if ( item.is_valid() and this.is_valid() and this.is_compatible_with(item) ) {
            ...
        }
    }
    ....
}

Now Test is closed under merge, for merge always returns an instance of Test. Additionally, the valid field allows it to skip the merge for both incompatible data and invalid Test instances, thus propagating problems. Now I can write:

    e = a.merge(b).merge(c).merge(d)
    if ( e.is_valid() ) {
        ...
    }

If the merge between a and b is invalid, I get an instance of Test with the invalid flag set, which is passed onto the merge with c, but since it’s invalid, the merge is skipped and a new Test instance with the invalid flag is set and so on.

I also set Test’s default value to invalid, since it makes sense that an uninitialized Test would be invalid; the behavior of an uninitialized Test under merge is also exactly what I want — notice how I analyze how any change impacts the relationships formed by my methods.

Extending data values to cover errors may seem like a hack, but is it so? If this gives us well defined semantics in all cases, increases reliability and makes it easy on callers, then isn’t this superior to the alternative? Remember the lesson of 0! In fact, 0 has additional interesting parallels here. First, while it’s part of the set of numbers, in a way it’s not “really” a number, much like an invalid Test object is not “really” a Test object. Additionally, 0 propagates under multiplication like an invalid Test propagates under merge. Just like the result of a merge is invalid if any component is invalid, so is the result of a string of multiplications 0 if any component is 0. Even the initial state of Test being invalid is similar to numbers starting at 0.

But there’s a deeper lesson. Abstractions are not just a grouping mechanism for data and functions; they are systems with their own necessary relations, foremost of which is closure. Other relations can be defined by the nature of the abstraction. Therefore, when thinking abstractions, think relationships. As long as we treat abstractions as an afterthought, we will continue to suffer from poorly designed libraries and brittle code.

This applies to any paradigm; for instance in functional languages, we can use algebraic data types to add an error that’s part of our data. The real issue is extending our systems to achieve closure, and giving people completely defined semantics. The motto should be 100/100. 100% defined 100% of the time. There should be no way to “jump” outside the system.
 

Further Reading

The New Mathematics by Irvin Adler is a great illustration not only of the development of mathematical systems, but the power of abstraction. He starts from counting on one’s fingers all the way through complex numbers and takes a digression with non-numerical math that satisfies the very abstract properties of groups!

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s