On the choice between “error” and “do something reasonable” in design

As some of you noted in your implementations, you faced a choice between using (error ..) and returning a default or nil-potent value of some kind (like zero, empty list). This is common choice point that we need to face when designing functions. There is relatively little in terms of “science” to this though and in the case of user interfaces is more “art” than “science”. To the extent there is, here are some thoughts –

A function is said to be “total” in the domain of its arguments, if for every possible argument it terminates with a valid value in its co-domain. Total functions are easy to program and reason with. However they’re somewhat of a rarity as well. Given that the reason to lean towards total functions is that they become easier to reason with, that ought to be useful as a criterion to go for making a function total by specification. i.e. if making it total is not making it easier to reason with, then there may be benefit to introducing error behaviours.

One way a function can be (somewhat artificially) made total is to make its result a “sum type” or (in Racket teminology) a “union” of possible types. For example, we’ve used (U False Value) as the result type of lookup functions. When choosing this, consider whether the types involved in the union have any overlap. It is easier to reason if they don’t overlap (i.e. are “disjoint”). For example, if Value can include Boolean, then (U False Value) is superfluous and not informative or useful as a type. Go for a sum type result if the function cannot know whether the conditions under which it returns the various types are true “errors” as far as the program goes and the caller has to decide that. In some cases, the caller may choose to wrap such a function into a total function and in other cases the caller may wrap it to raise an error. An example of this is our “lookup” function that may give you a value associated with an identifier. Go for an error if there is some contract that would be violated if some property of the arguments does not hold. This is often the case in writing interpreters where you (usually) want to fail early on encountering an invalid program instead of trying to do something “reasonable” with invalid input. One of the points in favour of compiled languages (especially those with good type systems) is to signal “bad program” BEFORE the program gets to run and (maybe) cause damage – think programs used in nuclear reactor control or X-ray machine control. We’ll discuss type systems soon in this course.

So far, we’ve only used (error ...) in a way where our programs bomb totally and terminate on encountering an (error ...) expression. In case you missed a central point of this course – this is also a design choice! For many kinds of target domains, such “unwind the stack until you reach a handler or terminate” is a usable and efficient means of dealing with such errors. Such an “unwinding of the stack” is a simple “one shot continuation” that can be efficiently implemented on most hardware at O(1) space and time cost. Some languages like Common Lisp don’t unwind the stack, but give control to the higher level error handlers so they can decide what to do – whether to terminate the program, or “bubble up” the condition to the next handler, or make a correction and resume or restart the operation that produced the condition with a different starting point. This is a powerful tool in the hands of good programmers who know that higher up in the call sequence there is more information available to decide what to do about an error condition than down in the deeps of the call sequence. Common Lisp therefore does not call these “errors” and uses “conditions” to talk about them, because the handler may choose to ignore a condition if it sees fit. A limited version of this flexible approach was also useful in the context of my own muSE dialect of Scheme used to express video editing styles - https://github.com/srikumarks/muSE/wiki/ExceptionHandlingLinks to an external site. .

The language Erlang and its newer protege “Elixir” use “processes” (memory-isolated concurrent distributed computational units with a message queue) as a primitive. Processes are cheap in Erlang and can cost as little as 300 bytes compared to about 1MB for a thread in, say, C++. So the Erlang design philosophy encourages a kind of “happy path” style - where it is considered perfectly reasonable to “just crash” on error. Only the process running the code will terminate and Erlang libraries (called “OTP” for Open Telephony Platform) provide utilities and patterns for how to handle such crashes to build error resilient systems – through restarts and process “supervisor trees”.

You can design arbitrary control structures using reified continuations. So you could work out the details of the ideal control mechanisms for a particular domain and model them using continuations to understand their semantics before you commit to an existing mechanism such as “exceptions”. You’ll then be in a position to decide whether a new control structure is appropriate or an existing one will do. In most of the common programming languages, the language designers have made some reasonable default choice of mechanism for you. You’re now a language designer too, so you may find their choice “unreasonable” in your context and can do something about it.