Showing posts with label Methodology. Show all posts
Showing posts with label Methodology. Show all posts

Friday, July 17, 2015

Programming by Thinking

In my second post (Programmers as Children) I complained about the fact that programmers work with their fingers instead of using their brains.  This is gradually getting worse instead of better, with growing academic research concentrating on the use of examples from the internet, search tools to find such examples for specific questions, and so on.  Of course, there are many websites containing advice for various programming domains, and the examples they provide are often quite useful.  However, copying these into your own code without understanding what they do, and what are the wider implications (for example, on performance and security), is a recipe for disaster.

The April 2015 issue of Communications of the ACM contains several articles that should be required reading for every software developer.  The first is Leslie Lamport's viewpoint article, "Who Builds a House without Drawing Blueprints."  No engineering discipline proceeds to build anything without first analyzing the requirements, making initial designs and analyzing them from various points of view (such as cost, safety, standards, and legal implications) -- in other words, thinking deeply about the problem and possible solutions.  But in the (arguably misnamed) discipline of software engineering, starting with coding is all too often the norm, and is even praised as a methodology.  (I am not implying that any of the agile methodologies advocates that; quite the contrary!  However, agile methods are often misunderstood as "don't think before you code.")

Lamport argues that thinking is necessary before coding, and that writing specifications is an excellent way to focus your thinking.  The specifications need not always be formal, although Lamport makes a good case for using formal specifications, and using tools to check them.  Specifically, TLA+ is a formal specification language created and used by Lamport himself in various projects.

You may be inclined to dismiss this as only academic, and, unfortunately, this is the too-prevalent view.  However, you may learn otherwise if you read another article in the same issue, "How Amazon Web Services Uses Formal Methods."  This article describes how TLA+ was used in several critical Amazon projects, and discovered bugs that the authors admit they could not have found otherwise.  In some cases, Amazon engineers used TLA+ successfully on their projects after two weeks of self-learning.

Of course, getting the specifications right does not mean that the code will be correct as well.  Tool support for verifying the code against its specification would be very helpful, and this is an area of active research.  However, not getting the specifications right is almost a guarantee that the code will have bugs.  This is particularly true for large concurrent systems, which are a source of extremely subtle bugs that programmers have great difficulties understanding and predicting.

And while you have the April issue in your hands, I recommend you also read the article "Security Challenges for Medical Devices."  It discusses safety, security,  and privacy issues with various kinds of medical devices.  Even hospital information systems have such issues, but they are most critical with implanted devices such as pacemakers.  Modern implantable devices communicate wirelessly to provide information they collect, and sometimes they can even be programmed wirelessly.  This creates health hazards from mistakes but also deliberate attacks.  Even devices that only provide information are dangerous if this information is modified anywhere along the way from the device to the physician (including in IT systems that store or transmit this information), since this could lead to the wrong treatment being applied.

Security cannot be added as an afterthought; it must be designed-in from the beginning.  While not all systems have the same security implications as medical devices, the whole chain is as strong as the weakest link, which implies that security is crucial to more systems than would be immediately obvious.  This helps drive home Lamport's point.  Think before you code!

#formalmethods #specifications

Monday, March 10, 2014

Review: Mars Code



Despite a few spectacular failures (such as the Mars Climate Orbiter and Mars Polar Lander), NASA is one of the top organizations worldwide in Systems and Software Engineering.  The Mars rover Opportunity has recently celebrated ten years of operation on Mars, and the newer Curiosity has been operating since August 2012, after a landing maneuver of unprecedented complexity.  This is quite a feat, considering the fact that the nearest repair technician is over 30 million miles away, and communications delay is about 15 minutes on average.  How do they do it?

The paper Mars Code by Gerard J. Holzmann (Communications of the ACM,  Feb. 2014) gives a rare glimpse into the process used in programming the Mars rovers.  It focuses on three methods used to reduce the risk of failure.  An important feature of all three methods is the use of automation.  The first consists of a set of risk-related coding rules, based on actual risks observed in previous missions.  For example, one rule requires the code to pass compilation and static analyzer checks without any warnings.  Another requires the code to have a minimum assertion density of 2%.  The assertions are enabled not only for testing, but also during the mission, when violations place the rover into a safe mode, during which failures can be diagnosed and fixed.  All the rules are checked automatically when the code is compiled. 

The second method consists of tool-based code review.  Four different static-analysis tools are used, with an automated system to manage the interaction between developers and reviewers in response to tool warnings.  In over 80% of the cases, developers agreed with the warnings and fixed the code without reviewer intervention.  An interesting observation Holzmann makes is that there was surprisingly little overlap between the between the warnings issued by the four tools. Apparently, each tool has its own areas of strength, and they are different enough that it is useful to use them all.

The third and strongest method is the use of a model checking tool for race conditions, one of the most difficult types of bugs to discover.  This cannot be done for the whole code base, but was used extensively in the Curiosity mission to verify critical multithreaded algorithms.   In some cases, the tool works directly from the C source code.  For larger subsystems, input models had to be prepared manually in the language of the model checker; this can reduce the input size by over one order of magnitude.

The challenges faced by NASA are obviously unique, and are very different from those involved in cloud-based mobile applications, to take a popular example.  There are many papers and books describing methodologies for developing such systems.  However, developing safety-critical systems for cars and aircraft, medical devices, and power grids, or financial systems that must be resilient to faults and deliberate attacks, requires reliability levels similar to those of NASA.  For developers of such systems, papers such as Holzmann’s should be required reading.

A related article you may want to read is They Write the Right Stuff, by Charles Fishman.  This article, from 1996, describes the culture of the group that wrote the software for the space shuttle, and how their development process led to the very high quality of their product.  For example, whenever a mistake was found in the code, it was traced to the corresponding problem in the process that allowed the mistake to be written and pass reviews and testing.  Then, the team investigated what other bugs could have been caused by the same process failure.  In this way, a bug in the control of the shuttle's robotic manipulator arm was traced back to a certain type of problem.  When investigating other effects of the same problem, a much more serious bug was found in the control of the high-gain antenna.  The bug was fixed before it had a chance to manifest itself.

It takes a lot of discipline and investment to work this way, but the result is a much better, more reliable product.  Sometimes it isn't all that important; other times, it's the difference between life and death.

Sunday, February 19, 2012

Refactoring

Change is a way of life in software development.  Requirements change because clients gain a better understanding of their needs (possibly due to experience with an existing version of the software), to take advantage of new business opportunities, or for external reasons such as new legislation that requires new types of auditing, or change in standards.  The environment may change in various ways; for example, moving from a private data center to a public cloud, operating systems may become obsolete, or mission-specific hardware may need to be upgraded.  Change is the greatest challenge to the software development process, and development methodologies employ various tactics to accommodate it.  The classic "waterfall" methodology tries to eliminate it, by spending a lot of time in the beginning of the project trying to get the requirements just right.  Unfortunately, even if this is extraordinarily successful, by the time the other steps in the process are done, the requirements are practically guaranteed to be out of date.

Agile methodologoies take the other extreme.  Significantly, the classic introduction to Extreme Programming (nicknamed XP) by Kent Beck is called Extreme Programming Explained: Embrace Change.  These methodologies accept that change is inevitable, and gear every activity to support it.  This can only work if it is possible to modify existing software to fit changing requirements, and this is only possible if the software is always well-designed.  Unfortunately, software tends to change away from a good design, because each change is usually done without much regard for the overall design.  As changes accumulate, it becomes harder and harder to make further changes, and the likelihood of introducing errors grows in an exponential manner.  Therefore, one of the basic tenets of agile methods is that developers must take the time to restore a clean design to their software every once in a while.  This activity may seem to be unproductive, since it doesn't result in any modifications to the external behavior of the software.  However, it enables further modifications, and is therefore essential.

The activity of changing the internal structure of the software in order to restore it to a well-designed form without affecting external behavior is called refactoring.  First introduced by Bill Opdyke in his 1992 PhD thesis, and popularized in the famous book Refactoring: Improving the Design of Existing Code, refactoring is an approach to software development as well as a set of "refactorings," each of which is a detailed list of instructions on how to perform structure-modifying but behavior-preserving changes in code in a reliable way.  Reliability is achieved by performing unit tests after each small change, in an attempt to guarantee that behavior is still preserved.  If some of the tests fail, it is easy to identify the reason, since it is rooted in the last small modification performed.

Of course, in order for this to work, it is necessary to have an extensive suite of unit tests, which will guarantee (with high probability) that any change that doesn't preserve functionality will be discovered immediately.  This is easier said than done.  A good set of unit tests may be considerably larger than the production code itself, and requires significant effort to create and maintain.  Most agilists consider this to be a necessary evil; I once heard a developer say he enjoys writing these tests, and even Kent Beck, the high priest of XP, expressed his surprise at that.

Still, developing without a good suite of tests is like driving blindfolded; you can't see where you're going and you're afraid to make any change at all.  In fact, Michael Feathers, in his book Working Effectively with Legacy Code, defines a legacy system to be one for which you don't have a good suite of unit tests.  And refactoring is a very effective and satisfying way to program.  I recall several experiences in which I started making some change in my code, which led to further changes, leading to a period of several days when I couldn't even compile my code, let alone run it.  This is a terrible feeling; you know you have problems in your code, but you have no way of testing it to find out what they are.  At the end of this process, when I got back to a running system, it indeed had problems, and it took a long time to find them all and fix them.  By restructuring the work into a series of refactorings, it would have been possible to test much more often, and thus find problems and fix them much earlier.

As I said, there can be no agile methodologies without refactoring.  However, refactoring is a useful activity regardless of which methodology you use (including no methodology at all).  It does come at a cost; you must have an extensive test suite, and refactoring itself takes time and effort.  I consider this cost to be well spent, since it saves much more effort down the line, when the inevitable requirement changes appear.  Hard as it is to convince programmers to invest in their future (see Programmers as Children), this is one of the important places to try.

As in other cases, tools are a great help in following the methodology, and therefore also in convincing programmers to use it.  Furthermore, good tools take away some of the burden of checking the correctness of the transformation.  Good refactoring support exists (mostly for Java) in Eclipse and other modern IDEs, and I urge every developer to make the maximum use of these.  When I modify my code, I make every effort to use the automated refactoring capabilities provided by the IDE, as well as the related source transformations and quick fixes.  I do this even when this forces me to make the changes in a certain way or order, just to take advantage of the automation (and related reliability) in the IDE

Unfortunately, all modern IDEs have bugs in their refactoring support, chiefly due to insufficient analysis.  For example, they might create variable bindings that shadow existing bindings incorrectly.  So you must be careful when using these tools, and be aware of their limitations.  In spite of these limitations, I find them extremely useful.  One of the current research interests of our group is automating more powerful refactorings and making them more reliable; more about that in a later post.

Tuesday, February 1, 2011

Responsible Adults

If some programmers are not behaving maturely (see Programmers as Children), what can we do as responsible adults to make things better?  There are many answers, and they depend on who is meant by "we".

First, however, I must mention what is perhaps Fred Brooks's best-known paper, "No Silver Bullet: Essence and Accidents of Software Engineering" (IEEE Computer, April 1987).  In it, Brooks claims that software engineering is hard in essence, and therefore nothing can make it easy.  While a lot of progress has been made on what Brooks calls accidental difficulties, which have to do with the way represent the abstract concepts of software engineering, these only expose the essential difficulty of the conceptual structures underlying software development.  The silver bullet is the only effective way to kill werewolves; Brooks claims that no such magical cure can be found for the ills of software development.

This is, of course, a provocative statement, and it sparked lively discussion.  But in spite of various opinions to the contrary, no silver bullet has yet been found.  This is not to say that progress has not been made.  Even if the essential problem is difficult, programmers are still being asked to develop larger and larger systems.  They should use whatever cures are available, even if they cure only the symptoms and not the disease.

So what can we do?  The most important thing is to be aware of the difficulties and look for ways to avoid them.  There is no substitute for careful thinking, and no tool will do that for us until true artificial intelligence is available.  (While great progress is being made – for example, see the Watson Jeopardy challenge – don't hold your breath waiting for "automatic programming" in the sense of the computer writing its own programs.)  More concretely, there are many practices we can follow in order to make software development easier, more productive, more reliable, and also more fun.  These fall into several categories, including methodology and process, languages and tools, and education.  I will have something to say about each of these, but only a book the size of an encyclopedia can do justice to them all.

There are many software-developoment methodologies, and none is perfect.  In fact, Daniel Berry of the University of Waterloo has a paper titled "The Inevitable Pain of Software Development: Why There Is No Silver Bullet".  Another provocative paper, and one of the responses to the one by Brooks, this one focuses on why no methodology can ever be a silver bullet.  Berry claims that every methodology has some inherent pain associated with it.  It may be all pain, as in the maligned waterfall method, or the pain could be just a small part of it.  In any case, it is unavoidable, and developers therefore evade it by not following the methodology rigorously, thus defeating its purpose.  But this is like parenting: although no one knows of a sure method to raise perfect children, that doesn't keep us from trying.  In the same way, the fact that there is no perfect development methodology doesn't mean that we should not attempt to find the best process for our situation and follow it to the best of our ability.

As I mentioned before, I'm a great believer in tools, and spend a lot of time developing new tools.  I have also been involved with computer-science education, at both the university and high-school levels, and I have a lot to say about these topics.  Stay tuned!

Sunday, January 2, 2011

It Looked Good When We Started

"Well, in our country," said Alice, still panting a little, "you'd generally get to somewhere else — if you run very fast for a long time, as we've been doing."
"A slow sort of country!" said the Queen. "Now, here, you see, it takes all the running you can do, to keep in the same place. If you want to get somewhere else, you must run at least twice as fast as that!"
— Lewis Carrol, Through the Looking-Glass, and What Alice Found There  

The Red Queen's race is often quoted in discussions about the increasing rate of change in our lives, which demands running just to stay in the same place.  This is particularly true in software development, where change is the only constant factor.  Any successful software system will need to be changed, and, if it can't, will fall by the roadside.  Change can be due to many factors, such as new legislation, new standards, new business opportunities, the need to support a larger volume of business, or the need to move to a new platform (for example, when an operating systems is no longer supported by the vendor).  The introduction of a software system into an environment that didn't have one before changes that environment; that change ripples to create new requirements.  Perhaps initially the system was only meant to replace manual work, as when using computers to track customer orders instead of doing it on paper.  But once the system is in place, users realize that it can do much more; for example, letting customers enter their own orders through the internet.  Adding that functionality may trigger the idea that customers may now be able to track the order fulfillment process through the internet.  Every change is the catalyst for another one.

There is a common misconception that software is easy to change.  It is certainly true that distributing software updates is much easier (and cheaper) than changing hardware.  However, making changes in software correctly while preserving previous functionality and without introducing new bugs is very difficult, as all experienced software developers know.  (In spite of that, they are often content to get their software more-or-less working, and rely on the users to discover problems and on future releases to fix them; see Programmers as Children.)

Obviously, it makes sense to prepare for future changes, so as to make the inevitable changes as easy to make as possible.  Some requirement changes, however, are beyond prediction, and many predictions that seem obvious at the time turn out to be wrong.  These unfortunate facts have far-reaching implications on all aspects of software development.  They mean that it is impossible to capture the precise system requirements, since these will change unpredictably.  They also mean that it is impossible to prepare for all future changes, since every design decision renders some future modifications easier while making others more difficult.  But they do not mean that we should just build for known requirements and ignore the possibility (or rather, inevitability) of future changes.

The Agile Development community takes an extreme view of our inability to predict future changes, and uses the rule "do the simplest thing that could possibly work" (abbreviated DTSTTCPW).  This rule is based on the assumption that keeping the code as simple as possible will make future changes easier; it also assumes the meticulous use of refactoring to keep the code as clear as possible while making changes.  (I have a lot to say about refactoring, but that will have to wait.)  However, there are some decisions that are very difficult to back out of, and I believe that most agilists will agree that planning ahead on these decisions does not violate DTSTTCPW.

One of the earliest decisions that must be made, and one that has a profound influence on all aspects of the development process, is about tooling, including the programming languages to be used.  Changing the language after a considerable amount of code has been written is very difficult.  Changing languages in a system that has been in operation for over twenty years and contains several million lines of code is almost impossible; this almost amounts to a complete rewrite of the system.  Much of the knowledge about many such systems has been lost, and the original developers are probably doing other things, possibly in other companies, and may even be retired.  Yet there are many large-scale critical systems that are over twenty and even thirty years old.

This received world attention in the 1990s, with the famous Year 2000 (or Y2K) Problem.  Programmers in the 1960s were worried about the size of their data on disk, since storage sizes were much much smaller in those days.  It seems incredible that today you can get a terabyte disk for well under $100; in those days you would be paying by the kilobyte.  So the decision to represent dates using just two digits to represent the year was very natural at the time.  In fact, it can be argued that it was the right decision for the time.  It was inconceivable that these systems would last until the year 2000; it was obvious that they will have to be replaced well before then.  Programmers kept to this design decision during the 1970s, 1980s, and, incredibly, even sometimes in the 1990s, although by then storage sizes have grown, prices came way down, and 2000 was not the far future any more.

To the amazament of the 1960s programmers, some of the systems they wrote then are still in operation today.  Naturally, these survivors contain some of the most critical infrastructure used by banks, insurance companies, and government bodies.  All of these had to be revamped before 2000 (actually, some even before 1998; for example, credit cards issued in 1998 expired in 2000).  This cost an incredible amount of money, and quite a few companies made their fortunes from Y2K remediation.  The fact that this effort was largely successful, and none of the doomsday predictions made before 2000 came to pass, should not detract from the importance of the problem and the necessity of fixing it.

Now switch points of view, and think twenty years into the future.  The code we are writing today is the legacy of the future; what kind of problems are we preparing for ourselves or the people who will be saddled with these systems after us?  I claim that we are being as incosiderate as the programmers of the 1960s, and with less justification.  After all, we have their lesson to learn from, whereas they were pioneers and had no past experience to draw on.

These days, we are building systems using a hodgepodge of languages and technologies, each of which is rapidly changing.  A typical large-scale project will mix code in Java for the business logic, with Javascript for the browser front end, SQL for the back-end database, and one or more scripting language for small tasks.  A legacy Cobol system may well be involved as well.  The Java code will be built on top of a number of different frameworks for security, persistency, presentation, web services, and so on.  Each of these will change at its own pace, necessitating constant updates to keep up with moving technologies.  How will this be manageable in twenty or thirty years?

Think of a brand new application written by a small startup company.  They are very concerned about time to market, but not really thinking twenty years into the future (after all, they want to make their exit in five years at the most).  And their application is quite small, really, just perfect for a scripting language such as Perl, Python, or Ruby on Rails.

If they fail, like most startups, no harm has been done.  But suppose their idea catches on, and they really make it.  They keep adding more and more features, and get more and more users.  Suddenly they have many millions of users, and their system doesn't scale any more.  Users can't connect to their site, transactions get dropped, and revenue is falling.  Something must be done immediately!  The dynamic scripting language they used was very convenient for getting something running quickly, because it didn't require the specification of static types, classes could be created on the fly, and so on.  But now that the application consists of 20 million lines of code, it is very difficult to understand without the missing information.

This scenario is not fictional; many successful internet companies went through such a phase.  In fact, even infrastructure tools went through such changes.  For example, consider PHP, one of the most successful web scripting languages today.  PHP 4 was downward compatible with PHP 3, although it had a number of serious problems, because it was deemed inappropriate to disrupt the work of tens of thousands of developers.  PHP 4 was wildly successful, with the result that when incompatible changes had to be made in PHP 5, hundreds of thousands of developers were affected, and adoption of PHP 5 was delayed.

I think that many times developers use the wrong tool for the job (see The Right Tool for the Job) because it looks like the right tool when development starts.  Changing circumstances then make it the wrong tool, but switching tools can be extremely costly.  With hindsight, it was obviously a mistake to base the whole application on a scripting language in the scenario described above, even though it looked like a good idea at the time.  Can we use foresight to avoid such mistakes in the future?

Monday, December 27, 2010

The Right Tool for the Job

No professional plumber will think of using a wrench instead of a hammer (although quite a few amateurs wouldn't think twice before doing so).  In fact, professionals avoid adjustable wrenches, using the appropriate fixed wrench for each task.

Developers are much less picky about the tools they use.  Take, for example, the C programming language.  It was designed for the development of the Unix operating system, and is well suited for that task.  It allows low-level access to the computer's resources, it can be efficiently compiled into efficient code, and requires very little run-time support while not being tied to any specific hardware platform.  In essence, it is a high level, machine independent, assembly language.  Of course, these properties, extremely desirable for writing system code, required the language designers to make various choices that make C extremely unsuitable for most other programming tasks.  For example, the desire for efficiency and free access to all memory led to unconstrained array access, without any runtime bounds checking.  This has enabled the infamous buffer-overrun attacks, so favored by writers of viruses and other malware.  In turn, these require extra care by application developers who use C or C++ (which, unfortunately, preserved this and many other undesirable properties of C), and necessitated the development of various tools for checking C/C++ programs for these kinds of errors.

Why did this happen?  There are several reasons why programmers don't always use the right tool for the job.  One is the generality of our tools.  Usually, the same application can be written using any general-purpose language.  One may be particularly well-suited for the task, but, with more effort, others can be used.  Another reason is the steep learning curve associated with our tools.  It takes a lot of time to learn all the intricacies of a programming language (or other tool), and a programmer who mastered one will be reluctant to start from scratch with another language just to perform one task.  Obviously, the developers of the Unix kernel were masters of the C language (and influenced or even participated in its evolution).  It was natural for them to write all other parts of the system, including user applications, in the same language.  The great success of Unix and C created a large body of programmers whose hammer was C, and all applications their nail.  As a result, the first really popular object-oriented language, C++, was built as an extension to C, inheriting all the properties that make it unsuitable for user-application development.

It took a long time until a successor appeared, in the form of Java, which rejected many of the assumptions underlying C and C++, and removed many of its undesirable properties.  (Of course, we are still stuck with the awful syntax, which is responsible for a whole set of problems by itself.)  First and foremost, Java challenged the assumption that the language must be compilable into extremely efficient code; this is in fact wrong for most application development work.  Yes, inner loops in database management systems, communications, cryptography, and similar software must be heavily optimized.  But it is much more important for business (and desktop) applications to be free from security vulnerabilities than for them to be a little faster.  In addition, development expenses, which include developer time as well as the cost of tools that attempt to find security vulnerabilities and other problems, often outweigh the benefits of application speed.  Of course, loss of customer trust as a result of security breaches can be devastating.

We saw two reasons why developers don't always use the right tool for the job: the overlap between tools due to their generality, and the difficulty of learning a new one.  These have a corollary, resulting from the nature of organizations.  While it is good to have experts in various kinds of technologies and tools, it is important for a cohesive organization to have a common set of tools, a lingua franca that everybody is familiar with, for the main part of the development work.  This is important for good communications, and also makes integration easier.  But it also leads to the use of the least common denominator, the language and tools that can support most of the work, even if they are not the best for any single purpose.  Thus I find myself these days programming in Java, although it may not be my first choice for the kinds of systems I develop.

There are more reasons why developers don't use the best tool for the job, and many other tools worth discussing.  But these will have to wait for later posts.

P.S. See the excellent book, C Traps and Pitfalls, for a list of syntactic and semantic issues you must be aware of if you develop in C or a derivative language.

Tuesday, December 21, 2010

Children Programmers

No high-school graduate expects to get a job as a physicist, even after getting top grades in the advanced placement physics exams.  But I know of many high-school students who make money programming, and who think of themselves as professional programmers.  No large or knowledgeable company will use such child labor, but there are still many small businesses and other clients who don't understand what really makes a professional developer, and are glad to save some money this way.

If this is a small one-time job that doesn't require maintenance, it may even be successful.  But if it becomes necessary to make changes, especially after the programmer has graduated from high school and moved elsewhere, the client often needs to scrap the application and start from scratch, having learned something the hard way.

This is not the children's fault.  They really don't understand what being a professional programmer is about.  In fact, this is not always discussed explicitly in the university computer-science curriculum.  Yes, there are courses on software engineering as well as project-based courses, and conscientious professors require extensive tests and documentation even in other courses.  But there are many courses, with specific requirements in each, and it is very difficult to create a truly large project that really requires good software engineering practices in the undergraduate curriculum.

Without such projects, the exercise is self-defeating.  Students will do whatever is required to get a grade, which often means creating an impression of using the practices required of them.  But they will realize that this isn't really necessary for the project itself.  As a result, they will feel that software engineering is another of those things best forgotten after the exam.  Hopefully, they will recall what they learned when they start working for real, and encounter the problems that make disciplined development a necessity.  I still occasionally meet former undergraduate students who tell me that now that they work in industry they finally understand what I was talking about.

Being professional is actually much more than following some software engineering process.  It is taking responsibility for your work: pursuing quality, avoiding questionable practices, refusing to release dangerous products.  It is taking responsibility for yourself: keeping yourself educated, continuously learning, being honest with your co-workers, employer, and the public in general.  All these things can't be taught in one course, in the same way that children can't be educated to be responsible citizens in one year.  Both require lifelong learning, and teaching is best done by example.  And with so many adults behaving like children (see "Programmers as Children"), how can we expect the children to behave like adults?

Thursday, December 16, 2010

Programmers as Children

Freud is quoted as saying that the postponement of gratification is the hallmark of maturity.  Developers are put the the test very often, since the computer can supply instant feedback on our programs - just run it and see what happens.  And, unfortunately, developers often fail this test.  It is much easier to try it out than to think it out!

When I started programming, up to the beginning of my student days, we could only use punched cards for our programs.  (Professors and other privileged people could use "terminals"!)  You had to punch your cards, then put the shiny deck on the dispatcher's table.  This high priest of the computer would be reading a newspaper, and although he would notice you staring at him, would ignore you completely.  Trying to hint to him that he should put your deck in the card reader would only make him slower.  Eventually, he would perform this operation, and then you had to wait for him to fetch the fan-folded paper from the big noisy line printer, separate it into individual printouts, then put it in your box.  And then you would find out that you forgot a comma.

The whole process would take about half an hour of turnaround time (for a few seconds of actual computation).  This made us very careful when submitting a job.  And, since there is only so many times you can go for coffee in one day, we would spend the turnaround time running the program in our heads: trying to figure out what can go wrong with it and fixing it on paper.

I will be the first to admit that checking for syntax errors is best left to the computer.  It is a waste of a developer's time to have to scan his code for them.  But time devoted to thinking about a program is time well spent.  As an undergraduate student, I wasn't thinking about formally proving my programs correct, and I didn't have the formal training for it, but in essence that is what I was trying to do.  And the result was that I found many logic problems before ever submitting the deck.

In my last year of undergraduate studies at Tel Aviv University, I took a compilation course and had to write a small compiler as a course project.  I decided to write it using a language made for the purpose: SNOBOL4.  Unfortunately, Tel Aviv U didn't have a SNOBOL4 compiler (probably because there wasn't one for the CDC 6600).  But I was taking a graduate course at the Weizmann Institute in Rehovot, and had an account on their IBM machine, which had the SPITBOL compiler for SNOBOL4.  Being reduced to the use of public transportation, I only visited Rehovot once a week, to attend my course.  So even though I had access to a 3270 terminal there and had almost instant turnaround time, I had to spend a week just staring at my listings between sessions.  I used that time to think about my compiler, and as a result finished debugging the whole thing in just two sessions at the terminal.  All told, I spent less time on the project that I would have if I had daily access to a terminal.  (Having a computer all my own to do my debugging on was totally unthinkable at the time.)

I'm ashamed to admit that I now find myself hitting "run" to see what happens, without thinking things through.  If it fails, I have learned something with little investment.  But what if it "works"?  Can I be convinced that my last change really eliminated the last bug, and I can go on to work on other things?  I still need to think carefully and convince myself that it will work on all other cases.

Testing is a useful way of increasing your confidence in your code.  But testing is not a replacement for thinking about the logic of your program!  What's more, writing good tests is a process that requires time and thought on its own.  And, like mental reasoning about programs, it is a process that many developers neglect.  But, more on that some other time.  I've got some code I need to think about.