The building of robust software

I just had an interesting phone conversation with a friend of mine concerning the building of robost software (architectures, applications) and how the process could relate to language design.

We started listing a checklist of criteria (whether required or merely wanted) and we came up with a few items. We had a simple criteria of program correctness, program maintainence (comprehension), and modularity.

For program comprehension/correctness:
Strong typing
Purely functional
Strict evaluation
Design by Contract came up as a possibility
Verbose error messages

For modularity:
Objects were a thought, but it was felt the same architectures could be achieved with types, functions, and modules

These were just a few of the things that we thought could help in building large scale applications and frameworks, but both my friend and I lack the desired amount of experience to develop a stricter and more verbose criteria.

I was wondering what everyone's elses opionion would be concerning this topic.

If you had to build a large software architecture/application what kind of 'features' would you look for in a language?

Best regards,

MJ Stahl

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

I'd be looking at some of the

I'd be looking at some of the languages that have managed to establish a significant track record in industry as being strong languages for large robust software projects. To me that means things like Ada (and high integrity derivatives of it like SPARK) and Eiffel and maybe Erlang. I expect there are other languages with similar track records in industry that I either haven't heard of or forgot, so hopefully other people can mention them.

One thing that I think is sometimes overlooked as a useful language feature in terms of correctness and maintainability is having a "One Way to Do it" principle - that is, the language tends to have one clear straightforward way of doing most things and a lack of cute tricks, cunning implementations, neat hacks etc. While it's always nice to find some interesting and cunning way of writing the same code (be it for compactness in code, compiler optimization or whatever) the benefits in terms of clarity and readability of "One Way to Do It" can be significant.

Manageability

For many high-reliability domains, it's not enough that applications do the correct thing. It needs to be possible to see at any given time that they are doing the correct thing, and get a good idea of exactly how they are doing it. For instance, take a business-critical transaction-processing application. Such applications need more than to process transactions correctly in terms of business rules. They also need to have viewports into their internals, describing transaction throughputs and latencies, resource usages, error-conditions and potential-error-conditions seen, and probably even periodic internal consistency checks that look for indications of programmer error at run-time.

Given the fallen nature of mankind, it's rarely enough to be able to prove that a program is correct. You need to be able to show that it's correct as well, and working well in the wild, where real OSes/VMs/databases/networks/hardware may not work quite as well as our abstractions imply they do.

From a languages point of view, such needs are a strong fit for aspect-oriented techniques, although even simple object-orientation can be made to work with effort. Aspects make it easy to build both viewports on internal state and "firewalls" that specify affordance rules for the modification of that state. Purely functional programming would seem to work less well, as the whole idea of "monitoring" is pretty closely tied with the idea of state. Also, it defeats the purpose for monitoring code to be lazily evaluated. Impure functional programming seems to work pretty well, or at least no worse than object-oriented programming, as Erlang shows.

One thing to note with aspect-orientation is that while monitors and firewalls are fairly easy to implement, those implementations are imperative in nature, rather than declarative. A language which allowed declarative specification of conditions to be monitored and checked against would be very strong indeed. Think of it as "Design by Contract", with auditing and breach provisions specifiable.

One other point to note is that high-robustness domains require construction such that failure of one component doesn't cause cascading failures to other components. That implies some fairly loose coupling, including usually asynchronicity. That makes everything described above much harder.

Graceful Failover

It's (relatively) easy to make robust software when there are clearly delineated inputs and outputs.

But the real world is messy.

I would make Design by Contract a Definite rather than a Maybe, and would want to look further into how exceptions were handled, including the ability to audit, examine and repair problems.

You've included Purely Functional, but some state is always necessary, perhaps handled in a clean transactional fashion whereby it is only committed if it is correct - but I work with databases so I would think like that.

Scalability and locality

It doesn't seem that there is a "one solution fits all" approach for all applications. However, for some, my experience is that two key areas can be important:

Scalability

It is often desirable for an application to evolve and grow controllably. For this I favour a rule based approach based on state changes. With today's hardware, it is possible to achieve this for every state change. Rules can be added or dropped, but their functionality rarely needs to be changed if designed properly - most mistakes are made with changes to existing code. Functional pattern matching seems to work well for rule conditions.

Locality

It is essential that the functionality of component elements is as localised as possible. Preferably, there should be as few global variables in a system as possible. Any navigation required by a function to establish its context should be by reverse references.



Finally for illustrative purposes, here is an example of a complete rule in an Ivory system application:

module MonitorMGPAccount where
{
include "prelude.is"
include "comms.is"
include "MGP.is"

monitorMGPAccount rule::Rule event::Expr ads::ADS eventPhase::EventPhase =
   case event of {
      AddPropertyEvent mgpConnection::MGPConnection #userId,
      AddPropertyEvent mgpConnection::MGPConnection #password,
      UpdatePropertyEvent After mgpConnection::MGPConnection #userId,
      UpdatePropertyEvent After mgpConnection::MGPConnection #password ->
         if hasProperty mgpConnection #userId &
            hasProperty mgpConnection #password then
            let userId::String   = mgpConnection.userId;
                password::String = mgpConnection.password;
                !account = firstRef (rule.accountSet.accounts)
                                     (\account::Ref -> (account.userId::String) = userId)
            in
               if account = NULL_REF then {
                  trace "monitorMGPAccount: no matching account";
                  mgpConnection.errorCode := MGP_INVALID_USER_ID
               }
               else
                  if (account.password::String) = password then
                     mgpConnection.sessionActive := True 
                  else
                     mgpConnection.errorCode := MGP_INVALID_PASSWORD
   }
}

This rule checks a user id and password for some kind of session. If successful, it makes the monitorable update:

                     mgpConnection.sessionActive := True 

Note the locality in that the account set is obtained from the rule object - in this case a direct object path:

rule.accountSet.accounts

Any behaviour dependent on a new active session can be triggered by another rule sensitive to the sessionActive property, and so on.

My list

concurrency with strong process isolation
monitoring of other processes
deep and well-debugged libraries
reliable tools
read-eval-print loop for interactive testing
conducive to buildng test suites & running regression tests
simple enough that the compiler & run-time can be understood by one person
allow programs to be incrementally built and modified (no long compile times)
oriented toward function composition

/Me agrees, my incomplete list

  • Fine-grained run-time replaceable components
  • Light-weight memory-protected concurrent thread model with fail-over management and multi-mode
  • Backward tracing and tracking of requirements and design decisions from code
  • Strongly typed, eager, simple, impure, reflective, resource aware
  • Automatic documentation generation from code
  • Automatic test and measurement documentation generation
  • Interactive monitoring/testing
  • Life contract management, and life contract debugging
  • Graphical debugging tools
  • Time-freeze, repeat, backtrace facilities

Robust or Defect free? Make up your Mind.

Looking at your list I suspect you mean "I want to do as much static analysis to detect defects at compile time".

Robust to me means, "Shit Happens, both in my own code, and the code and systems that my program uses and/or communicates with. But my programs can deal with it."

The simplist and oldest Robustifier is the "Auto-save" on many editors. So even if the editor dies, not all your work is lost.

Another example is in asserts. Should asserts be on or off in production code? Asserts are code too, so assert expressions also have bugs. Leave them on, and your code will fail in production more often than it needs to, and many harmless failures will be converted to dramatic failures. ie. Makes code more fragile, less robust. Leave them off, and bugs are masked. (My answer is leave them on, but they should only log the failure and carry on as if nothing happened.)

You list static typing, I would prefer Duck Typing for robust programs. If it walks like a duck, if it quacks like a duck, we can treat it as a duck...

Good Exception handling is an important component of making robust systems. Exceptions should be caught at some level and handled, not allowed to propagate up until the program crashes, is rendered inoperable or all data is lost.

The "strict about outputs, forgiving about inputs" principle is vital for robust systems.

Is robustness appreciated?

Robustness is not always appreciated in our "stack it high, sell it cheap" culture - the tendency has been towards throw away and replace.

However, I thoroughly agree with the comments about "Duck Typing". I have a suspicion that why so many new languages are springing up represents an inherent dissatisfaction with the data modelling capabilities of many existing offerings.

Forgive, but grumble

The "strict about outputs, forgiving about inputs" principle is vital for robust systems.
I would apply your own advice about asserts here - be forgiving, but make sure your complaints of contract violations are noticed (here is a thin line between murmuring too softly so nobody notices and whining too loudly so the robustness is actually affected - e.g., the log file occupies the whole filesystem).

In real life there is a lot of various dispute resolution methods, should software engineering recognize the need for them? I imagine users setting up policies for software ombudsman :-)

Tough love

Being forgiving instead of strict leads to the mess that web development became. Better to publish a strict standard, and only accept input that conforms.

Channeling Jon Postel

Being strict instead of forgiving leads to stagnation, because no one will play in your world.

Better to publish a strict standard, and only accept input that conforms.

Better for the logical purity of the ecosystem as a whole, perhaps, but only if everyone agrees. Not better for you if you're one of a small minority of strict folks in a world where elephants, um, go where they will. :-/

Better for the logical purity

Better for the logical purity of the ecosystem as a whole, perhaps, but only if everyone agrees.

Indeed, which is why it's important to set the tone early, in the spec. XML did a good job with this by making the conformance check easy, so that the norm is to do the check.

Where I work we take XML input from customers, and we only accept input that passes the schema. We also accept other protocols for legacy reasons, and these are really a pain to work with because different clients break the spec in different ways.

Static vs. Duck Typing

John Carter: You list static typing, I would prefer Duck Typing for robust programs. If it walks like a duck, if it quacks like a duck, we can treat it as a duck...

Let's remember that "duck typing" == "structural typing" and is not in conflict with static typing: O'Caml is an excellent example of a statically-typed language with structural typing. An excellent example of some of the power that accrues to languages that support structural typing and correctly type binary methods can be found here.

I am not convinced that Duck

I am not convinced that Duck typing is equivalent to Structural typing. Duck typing would seem to have a number of different interpretations; some of which would be impossible to realise with a statically typed language. Classification by values is one.

Definition?

Alasdair Scott: I am not convinced that Duck typing is equivalent to Structural typing. Duck typing would seem to have a number of different interpretations; some of which would be impossible to realise with a statically typed language. Classification by values is one.

I'm curious as to what the definition of "classification by values" is. Offhand, it sounds to me exactly like type inference in a structurally-typed system (that is, a polymorphic function doesn't care, at all, about the name of the type of the value it takes, but only its "shape.") I think the confusion might arise because so few object-oriented languages differentiate between subclassing and subtyping—O'Caml is one of the rare ones, so if we can define our terms, it'll be interesting to see if I can craft examples in O'Caml. In the meantime, "On the (Un)reality of Virtual Types," referenced earlier, does an excellent job of demonstrating the flexibility of properly-typed binary methods and structural (sub)typing.

Dependent types?

I'm curious as to what the definition of "classification by values" is.

To me, it sounds like "classification of values based on values", or, to put it shorter, "types dependent on values" :-)

Classification by values

I was referring to classification based on, say, observed values. In simple CS terms, we might have something like:

Duck == Bird [observations: ["Waddles",
                             "Quacks"]]

Computationally expensive maybe, but not unreasonable. Others will remind me which languages implement this kind of approach!

Strictly Speaking...

... that's not "duck typing;" it's something quite a lot stronger that, AFAIK, only description-logics such as PowerLOOM, CLASSIC, RACER, or FaCT++ implement. They're extremely interesting, but fall outside the definition most people accept for "programming languages." On the other hand, one might be able to make the case that the popular definition needs revision...

Strong typing

If it walks like a duck, if it quacks like a duck, we can treat it as a duck...

Aha, Leibniz equality ;-).

Ocaml is my favorite language at the moment. One of the nice things about it is that you can get away with not supplying types for declarations.

I hardly omit types for declarations, though. I often find that supplying types helps me write better code, and adds a lot of readability to written code. Clarity is an essential part of a robust design so for a robust language I would prefer strong typing.

[Edit: Strong typing as in "every declaration of a term abstraction is explicitly typed by the programmer."]

Good point

You could write provably correct code, but that doesn't mean the program is robust. Robust would be handling the cases that you *forgot* to prove correct (or that you made errors in your proofs of), at least to the point of not bringing down the whole application. Robustness also implies being able to handle changes--even drastic changes--to file types and data structures without having to recompile the application.

So, yes, there are different types of criteria here.

readable syntax

I would add readable syntax to your list. At some point you will have to come back to a code section you haven't touched in months and change it. Maybe a bug fix, maybe a new feature. A language can help you here or get in the way.

Python is very easy to read, and thus I am more likely to find bugs in python code when I'm just doing a quick code review.

Now there are good arguments against python (most of the rest of your list for example). However in the real world readability and maintenance is often more important than all other considerations. (In very few cases will someone prove that their code is correct for example)

In fact maintenance is so important in the real world that you can make a good case for C++ or Java just because you can hire many programmers in those languages latter. Your initial release may take longer (and have more bugs), but at least you will have a better chance of hiring replacement workers latter. This is particularly important when hiring (or at least first round filtering) is done by HR, because HR departments have never learned that a good programmer can work in any language. (Note that HR is likely to demand more years experience in a language than the language has even existed, so you end up hiring people who are good at lying about their experience)

Thank you.

I would like to thank everyone for their comments. And I think I should have given reason for some of the items I put on my original list of criteria.

The typing issue stemmed from my new fascination with strong typing after using haskell and OCaml. I felt that they assisted me as a programming in making less mistakes and provided an extra layer of readibility when I came back to the code months later. Both I think are important criteria.

Purely functional was listed merely from a reasoning standpoint (with the thought that readibility and reasoning go under the umbrella of 'understanding' the code). And although I find the use of mutable state SO very useful I felt it could be added as a 'library', where the use of state would be explicit, hopefully relaying a small message to the programmer that mutable state comes with some 'baggage'. And although I wasn't too sure if concurrency was an important quality for such applications I figured having a purely functional 'core' language would be very benefitical when tackling concurrency at a future date.

Although I listed Design by Contract as a possibility, I merely mentioned 'possibility' because I was baiting a reaction from the community. I personally think that DbC is very useful and helpful (in regards to 'understanding' the code).

Best regards,

MJ Stahl