8/18/2005

Exceptions are part of the interaction

This blog entry discusses exceptions and their relationship to other interaction points. I here argue that the throwing and catching of exceptions should be regarded more or less as any other interaction between modules and thus conform to the chosen abstractions of those modules.

There is a constant debate about the virtues of checked exceptions. Checked exceptions mean that the potential thrower, or propagator, of the exception have to declare that potential act of throwing in advance, and that any users of the code have to either catch such exceptions or in turn declare itself as a propagator.

In the naïve use of this model, a ripple effect is seen, where either:

  1. A chain of client modules all have to declare the potential throwing of that same exception. Pass thru.

  2. One of the client modules have to handle that exception, potentially thrown from deep down the call chain.

The first alternative seems redundant and tedious. The second one is scary, since the upper-level client module might have little knowledge about the lower-level exception thrown. The typical example of the second scenario is a mapping abstraction throwing a NoSuchKeyException due to a non-existing key. That exception does not make much sense higher-up the call chain, where the module IncreaseEmployeeSalary is trying to deal with the fortunate event of an employee getting a raise.

It is quite understandable that developers, faced with these sad scenarios, tend to either silently ignore telling clients about that potential exception, where the language allows for such laziness, or use unchecked exceptions – a class of exceptions being so unpredictable that not even the implementer himself knows that it will be thrown. Unchecked exceptions often deal with resource availability issues, such as OutOfMemoryException.

In the face of having to deal with checked exceptions – after all, the serving code could have been implemented by someone else, not as lazy as the client developer – those developers would simply catch those, often lower-level, exceptions and do nothing, but perhaps log it. There is not much knowledge about the exception at that more abstract client level, so what else can one do?

Answer: One should treat exceptions with the same respect as any other interaction between modules, i.e., make it part of the API, at the same abstraction level as function calls.

This implies that the IncreaseEmployeeSalary module should not throw a NoSuchKeyException. Whether it informs its surrounding of that potential event – by declaring the throwing of that exception - or not is irrelevant to this discussion. That exception is simply at a completely wrong level of abstraction.

What should happen is that IncreaseEmployeeSalary is informed of an underlying problem, perhaps directly from that mapping abstraction, if the former happens to be a direct client of the latter. That event should in turn result in another event, possibly throwing an exception at the proper abstraction level of the former module, e.g., EmployeeNotFoundException. Note that there should not be an automatic “translation” of the lower-level to the upper-level exception, since the contract of the higher-level module might stipulate some other, non-exceptional, handling of that event.

Potential Problems
  1. Frameworks have no idea what the domain-specific exceptions would be.Yes, that is true, although one could in most cases wrap such framework components in more domain-specific code, which could turn the exceptions to more adequate ones. This is harder with template-based generic constructs, which are even mixed in via inheritance in some cases. This is why most hard-core C++ developers have given up on declaring exception throwing, but simply either wait for that unhandled exception handler, which often terminates the thread, or catch anything at a high level.

  2. How can you know what exceptions are potentially thrown from deep down?In environments forcing the declaration of such acts, there are ways. The important point here is that the implementer should know, either way, following the principle of treating exceptions as any other API element.Exceptions from deep down should be considered bugs.

  3. During debugging, at least, one needs to know what INITIALLY triggered this exception.Most language environments allow for nested exceptions, where a higher-level exception can carry the initiator as a sub object.

I am not stating that one should always convert each exception at each level. It might be the case that the low-level exception is fully adequate at the higher-level interface.

The main point is that the client should always understand the meaning of the exception thrown from the serving code, talking the “language” appropriate between the two modules. The same modularity aspects as with class methods, in other words.

8/17/2005

10 Steps to Software Solution Nirvana

These steps are applicable to any kind of programming task, and in most languages. Admittedly, Steps #6 and #7 are hard to achieve in most programming languages, but we can at least try…
I know that most of us have heard of these steps in various contexts, but the point is to actually follow them, strictly. It is possible and it is my firm conviction that they will make you a better problem solver. Steps #1 to #5 are pretty easily digested for most developers, whereas Steps #6 to #10 might no be.

Furthermore, the single most important step is Step #10. So, if you cannot follow the other steps, for any, political or not, reason, that is the step you should really contemplate and then take.

1. Never Jump
Do not use goto’s - not even disguised in the form of break’s of continue’s. The only exceptions to this rule are in switch statements and if you happen to be lucky enough to implement a threaded interpretation of an abstract machine.
Instead, if you want to affect the flow of control, please use those nifty built-in flow control constructs for conditional and iterative execution. If you are lost and really want to change the flow radically, raise some kind of error or exception signal. Refrain from extra-linguistic facilities such as longjmp.

2. Only One Exit Point
Do not use multiple (normal, i.e., non-erroneous) exit points from a modularized piece of code (function, methods whatever...) This obviously incorporates never having various return statements.

3. Avoid Massive If-Else
Do not use massive if/else’s to emulate a mapping. Represent that mapping instead, hashing or not. In some cases, the built-in core facilities such as switch are warranted, given the performance and small footprint, but those are exceptional cases.

4. Use Recursion
Yes, use recursion, rather than explicit iterations with managed state variables, i.e., watch out for excessive use of while and for. Recursion did not cease to be useful just because you left school. Do not be afraid of that stack biting you, unless you work in a very limited environment. After all, Conquer & Divide is not only a viable approach for heterogeneous (sub) problems…

5. Use Generic Operations
Abstract not only “things” but also “general operations.” These operations often represent crosscutting concerns in the domain and, in lack of proper AOP tools, should be implemented via higher-order programming. Those 50 years of Structural Programming should not be completely thrown away, in other words.

6. Abstract Functions
Use higher-order functions and functions as first-order “objects.” Could be viewed as a corollary to Step #5, but is really not. This is more focused on Functional Programming thinking. Functional Programming is definitely achievable in most popular languages, such as Python, Java and C++.

7. Avoid Variables
“What, is he crazy?” Perhaps, but it is my view that any explicit state-holding object is bad.
So, what should we use instead? Use higher-order composers. These composers are often manifested as function adapters.
Combine, or compose, constructs instead of passing information between constructs via variables.
For pure manifestations of no-variable languages, look at IFP (Illinois Functional Programming) or Super Combinators.

8. Avoid Index Variables
Do not increase index variables just for the fun of it. I.e., avoid simple for loops. Instead, use generic traversing constructs (foreach and such.) This is definitely a corollary of Step #7.

9. Avoid Inheritance
Let things cooperate instead of absorbing things by improper inheritance. The cooperation should preferably be setup in compile-time, for efficiency and proper type checking. This avoidance of inheritance is the latest fad in the object-oriented community. Nevertheless, it is a valid step to take…

10. Code As You Think
Use the same words when you code as when you describe, or contemplate, the problem and its solution. If those words do not exist in your programming environment, introduce them by extending the vocabulary (often definining…) This might sound simple, but most of us give in to an urge of steering the computer, instead of – declaratively – describing the solution. This requires the use of top-down definitions at times, where words (such as functions or types) that are not yet defined are used. Scary, initially, but powerful.

I will later supply examples of code complying and breaking these steps.