Until recently, programming languages have been developed with little consideration for programming environments and tools, with the notable exception of Smalltalk, which was co-developed with an elaborate environment from the start. Murphy-Hill and Grossman note in their paper "How Programming Languages Will Co-evolve with Software Engineering: A Bright Decade Ahead" (Future of Software Engineering, 2014) that this situation is changing, and provide seven predictions on the convergence of programming languages and software engineering (or, more precisely, programming environments and tools) in the next decade.
They predict that future programming languages will be designed with integrated development environments (IDEs) and social networks in mind. One example they give for the former is the addition of dot notation to F#, allowing a syntax such as x.Length in addition to the functional length(x). (Note, though, that in F# these have different meanings.) The authors claim that the primary benefit of this notation is the ability of the IDE to provide code-completion for method (or function) names when typing "x."; this facility doesn't (yet) exist for the functional notation. I have my doubts about this being the major reason for the inclusion of members in F#, but it could certainly be a reason for introducing dot notation as syntactic sugar for function calls in new languages.
Another prediction is that source code will no longer be limited to a single sequence of characters, instead being replaced by multiple simultaneous views. Multiple views are already available through the use of various tools; what I would call refactor-and-undo is probably the most widely used method, but there are various tools specifically targeted at showing multiple views of a program. These multiple views can be stored together as the official representation of the program, instead of being generated from a single "ground truth" consisting of ASCII (or unicode) strings.
Murphy-Hill and Grossman expect that future language design will be influenced by controlled experiments on how programmers use language features and to what extent. With the widespread availability of open-source projects, it is possible to study this rigorously, and select language features based on what really works "in the wild." This is already happening to some extent with existing languages, such as C#. I would advocate some care when using this method, since we know that some programming features take a long time to achieve adoption; a case in point is functional-programming techniques (see the last prediction below). We should be careful not to eliminate good ideas just because they don't receive a wide following in the short term.
Turning to other technologies, the next prediction is that formal verification will become a reality for developers, rather than being limited to a small group of highly experienced researchers. This will require the development of proof assistants that are stronger and more usable than those available today. Obviously, there is a lot of work to be done before the use of formal verification becomes widespread, but advances in this area provide some support for the authors' optimism.
Gradual typing is a technique that allows developers to write programs with little or no type information. As the programs mature, types can be added in order to raise the level of confidence in the correctness of the program by making more properties statically checkable. Murphy-Hill and Grossman predict that gradual typing will become more widespread in new programming languages, and will expand to include properties that can be expressed as types but aren't available in popular contemporary languages.
Existing programming languages are being used in new domains, such as parallel and distributed programming and big data, even though these languages aren't particularly suited to these domains. The success of Erlang is an example of how well a language built from the ground up to support distributed computation can do when applied appropriately. Murphy-Hill and Grossman predict that newer languages will focus more on new paradigms at the expense of traditional single-user (and often still single-threaded) applications.
This is related to the final prediction, which is that functional-progrmming concepts will become more widespread, and the distinction between functional and imperative (or object-oriented) programming will be more and more blurred. This trend is already happening, with the introduction of function objects ("lambdas") into popular languages such as C++, Java, C#, and Ruby, and with mixed languages such as Scala. First-class function objects are one primary feature of functional programming; the other is the use of immutable data, which is particularly important in distributed applications.
There is a recent (and not so recent) body of work that points in the direction of each of these predictions. Unfortunately, there are many influences on the design of programming languages, and various features of currently-popular languages were not really designed but added without serious consideration. I am therefore not quite as optimistic as the authors; however, the points they make are insightful and the paper is well-worth reading. Be sure to put a link to the paper in your 2024 calendar, in order to see which of the predictions will be realized by then!
Thoughts about software development and related issues.
The views expressed here are my own and do not necessarily reflect those
of any of my current or previous employers.
Labels
Agile development
(1)
AI
(1)
cognitive systems
(1)
Data science
(1)
Design by contract
(15)
ECMAScript Harmony
(1)
Eiffel
(1)
Extreme Programming
(1)
Formal specification
(1)
Functional programming
(9)
Haskell
(1)
IDE
(1)
Integrated Development Environments
(1)
interning
(1)
Java 8
(2)
JavaScript
(1)
Jupyter notebooks
(1)
Liskov Substitution Principle
(1)
Logic
(10)
metaprogramming
(2)
Methodology
(8)
Programming languages
(10)
Python
(5)
Refactoring
(1)
robopsychology
(1)
Scala
(1)
Security
(2)
super
(3)
Tools
(9)
Xtend
(2)
Xtext
(1)
Showing posts with label Tools. Show all posts
Showing posts with label Tools. Show all posts
Sunday, August 17, 2014
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, June 24, 2012
Design by Contract and Refactoring
I am a great believer both in refactoring and in design by contract. How do these two work together?
First, contracts are a great help when refactoring. The first thing you need to know when you refactor a piece of code is what it is supposed to do. This tells you what you can and can't do with it, and, no less important, what would be useful to do with it. If it's an arbitrary set of statements, say part of an existing method you are trying to extract into a new method, there's little to help beyond reading the code and trying to figure out what it does and how it fits with the rest of the code. (Even then, there are tools that try to deduce a contract for an arbitrary piece of code; more on that in a later post). But if it's a method, you should have more help.
If you have a good suite of unit tests, you can try to look at the tests of that method to figure out what it's supposed to do. But it's much easier if you have a contract attached to the method; the contract should give you a lot of the information you need. For example, suppose you want to move one or more methods and fields from one class to another (perhaps you are using Pull Up Method, Push Down Method, or just Move Method). In that case, you should check the contracts of moved methods to see how they fit in their new class. Are the invariants of the new class maintained by the moved methods? Are the contracts of the methods still valid, taking inheritance into account? Nasty bugs can result if the answers are negative.
On the other hand, refactoring may require corresponding changes in existing contracts, as well as the creation of brand-new contracts. In other words, contracts need to be refactored together with the associated code. This is an added burden, which may well be an obstacle to the use of the design-by-contract methodology; not only do I have to invest effort in creating the contracts in the first place, I also have to refactor them later. Why should I bother?
There are several answers to this complaint. First, consider the alternatives. You may be flying blind, trusting only on your undocumented understanding of the code. In that case, good luck to you; you'll need it. If you follow an agile methodology, such as Extreme Programming, you should have an extensive suite of unit tests to help you refactor. Unit tests are very vulnerable to change in the code, since they are attached to small units of code. This means that any shift in responsibilities is likely to invalidate some tests, which will then have to be refactored or completely rewritten. So there's always the need to refactor associated artifacts when you refactor the code.
Having contracts can significantly reduce the amount of detail in unit tests, and even the total number of unit tests. This is due to the fact that a major part of the responsibility usually given to unit tests is now taken by the contract. All that the tests need to do is exercise the system, but correctness checking (or a large part of it) is now done by checking the contracts. So now you can have higher-level tests that exercise the system, instead of unit tests for each class and method. For example, suppose you are implementing a cryptographic algorithm such as RSA. A test that creates a random key, then encrypts a random data buffer, decrypts it, and checks that the result is the original data, is guaranteed to exercise almost all of your cryptographic code. Moreover, this test can be used on many different implementations. In contrast, the internals of the implementation can vary widely, since there are many ways to implement the large-number arithmetic operations required for RSA, and their efficiency will be different under different circumstances. By having an application-level test augmented with contracts, the burden of refactoring tests is reduced to almost nil.
This still leaves the requirement of refactoring contracts, and the question of how much refactoring tools can help. In order to understand that, it is necessary to examine the relationships between code refactorings and contracts. On the simplest level, contracts are treated just like code. For example, when renaming a method, all references to it in the code must be appropriately modified; so must all references in assertions. Similarly, a method may be eliminated when it is not used anywhere, including in assertions. Contract-aware tools should find these kinds of relationships easy to perform automatically. Of the refactorings listed in Fowler's Refactoring
book, 32% do not interact with contracts except possibly in this way. These are mostly syntactic refactorings such as Remove Parameter or Rename Method, or those that eliminate classes or methods, such as Inline Class and Inline Method.
Some contracts affect the applicability of certain refactorings. As mentioned above, a method should only be moved if it won't violate the invariant of its new class. (Of course, it might be possible to modify that invariant to accomodate the moved method; this can be done in a separate step, before moving the method.) 13% of Fowler's refactorings have this nature.
Some refactorings require the creation of new or modified contracts. The most extreme case is Extract Method, which creates a new method out of arbitrary code. Discovering the contract for arbitrary code is impossible in general, although some tools can discover partial contracts. Another example is Create Superclass, which can create contracts for new methods based on existing contracts in subclasses. 59% of Folwer's refactorings fall into this category (which includes 4% that are also included in the previous one).
Finally, some new refactorings can be defined to deal specifically with contracts; these include Pull Up Contract, Push Down Contract, Create Abstract Precondition, and Simplify Assertion.
In a future post I will discuss automation of contract-related refactorings and how refactoring tools can take some of the burden of the contracts.
First, contracts are a great help when refactoring. The first thing you need to know when you refactor a piece of code is what it is supposed to do. This tells you what you can and can't do with it, and, no less important, what would be useful to do with it. If it's an arbitrary set of statements, say part of an existing method you are trying to extract into a new method, there's little to help beyond reading the code and trying to figure out what it does and how it fits with the rest of the code. (Even then, there are tools that try to deduce a contract for an arbitrary piece of code; more on that in a later post). But if it's a method, you should have more help.
If you have a good suite of unit tests, you can try to look at the tests of that method to figure out what it's supposed to do. But it's much easier if you have a contract attached to the method; the contract should give you a lot of the information you need. For example, suppose you want to move one or more methods and fields from one class to another (perhaps you are using Pull Up Method, Push Down Method, or just Move Method). In that case, you should check the contracts of moved methods to see how they fit in their new class. Are the invariants of the new class maintained by the moved methods? Are the contracts of the methods still valid, taking inheritance into account? Nasty bugs can result if the answers are negative.
On the other hand, refactoring may require corresponding changes in existing contracts, as well as the creation of brand-new contracts. In other words, contracts need to be refactored together with the associated code. This is an added burden, which may well be an obstacle to the use of the design-by-contract methodology; not only do I have to invest effort in creating the contracts in the first place, I also have to refactor them later. Why should I bother?
There are several answers to this complaint. First, consider the alternatives. You may be flying blind, trusting only on your undocumented understanding of the code. In that case, good luck to you; you'll need it. If you follow an agile methodology, such as Extreme Programming, you should have an extensive suite of unit tests to help you refactor. Unit tests are very vulnerable to change in the code, since they are attached to small units of code. This means that any shift in responsibilities is likely to invalidate some tests, which will then have to be refactored or completely rewritten. So there's always the need to refactor associated artifacts when you refactor the code.
Having contracts can significantly reduce the amount of detail in unit tests, and even the total number of unit tests. This is due to the fact that a major part of the responsibility usually given to unit tests is now taken by the contract. All that the tests need to do is exercise the system, but correctness checking (or a large part of it) is now done by checking the contracts. So now you can have higher-level tests that exercise the system, instead of unit tests for each class and method. For example, suppose you are implementing a cryptographic algorithm such as RSA. A test that creates a random key, then encrypts a random data buffer, decrypts it, and checks that the result is the original data, is guaranteed to exercise almost all of your cryptographic code. Moreover, this test can be used on many different implementations. In contrast, the internals of the implementation can vary widely, since there are many ways to implement the large-number arithmetic operations required for RSA, and their efficiency will be different under different circumstances. By having an application-level test augmented with contracts, the burden of refactoring tests is reduced to almost nil.
This still leaves the requirement of refactoring contracts, and the question of how much refactoring tools can help. In order to understand that, it is necessary to examine the relationships between code refactorings and contracts. On the simplest level, contracts are treated just like code. For example, when renaming a method, all references to it in the code must be appropriately modified; so must all references in assertions. Similarly, a method may be eliminated when it is not used anywhere, including in assertions. Contract-aware tools should find these kinds of relationships easy to perform automatically. Of the refactorings listed in Fowler's Refactoring
Some contracts affect the applicability of certain refactorings. As mentioned above, a method should only be moved if it won't violate the invariant of its new class. (Of course, it might be possible to modify that invariant to accomodate the moved method; this can be done in a separate step, before moving the method.) 13% of Fowler's refactorings have this nature.
Some refactorings require the creation of new or modified contracts. The most extreme case is Extract Method, which creates a new method out of arbitrary code. Discovering the contract for arbitrary code is impossible in general, although some tools can discover partial contracts. Another example is Create Superclass, which can create contracts for new methods based on existing contracts in subclasses. 59% of Folwer's refactorings fall into this category (which includes 4% that are also included in the previous one).
Finally, some new refactorings can be defined to deal specifically with contracts; these include Pull Up Contract, Push Down Contract, Create Abstract Precondition, and Simplify Assertion.
In a future post I will discuss automation of contract-related refactorings and how refactoring tools can take some of the burden of the contracts.
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.
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.
Monday, November 14, 2011
Runtime Checking of Contracts
As I said before, the most important use of contracts is methodological; by paying attention to contracts, you will be able to avoid many errors you won't have to fix later. Still, if you go to the trouble of writing contracts, you would like to get additional benefits. One of the most obvious is runtime checking of contracts, which gives warning about contract violations as they occur, helping catch errors close to their sources. Without contracts, a bug can corrupt a data structure or its contents, planting a time bomb that can explode much later. At that point, it is difficult to discover who made the erroneous change even with a debugger, since the offending method is no longer on the stack and can't be examined, and many other traces of the buggy action may also have disappeared.
In order to get these kinds of warnings, the contract must be written in some formal language; typically it is some extension of boolean-valued expressions in the programming language. Extensions are necessary in order to refer to "old" values; that is, allowing the postcondition to refer to values of variables or expressions on entry to the method. Rich assertion languages also add ways of expressing universal and existential quantifiers; often, you want to express some property of all the contents of some array or other data structure, or to assert that there is some element with a given property. However, it is easy to add such capabilities to languages that don't offer them simply by writing appropriate methods to check any required condition.
Runtime contract checking is complicated by several factors. First is the relationship between contracts on methods in inheritance hierarchies. As I explained in The Correct Use of Inheritance, inheriting methods may only weaken preconditions and strengthen postconditions. There are two approaches to enforcing this constraint. The one used by Eiffel uses syntactic enforcement, in which you can never write assertions violating this rule. In Eiffel, overriding methods use "ensure then" instead of "ensure" for postconditions, with the meaning that any assertion following "ensure then" is conjoined with any assertions from superclasses. For example, suppose you have a postcondition "ensure contains(x)" on the method Container.put(x); this ensures that after adding x to the container, it is indeed in it. A list is a kind of container, and there you want to add the postcondition "ensure then count = old count + 1", which means that the number of elements in the list has increased by one after adding an element. (This is not the case for sets, which are also containers.) The full postcondition of List.put(x) is now "contains(x) and then count = old count + 1". (The "and then" operator is a short-circuit operator, like && in the C family.) Similarly, preconditions in overriding methods use "require else" rather than "require", and add the new assertions by disjunction ("or else" in Eiffel, || in the C family.) While this approach prevents any violations of the inheritance rules, it means that you don't see the full contract in the class itself, and there could be hidden assertions you aren't aware of. This calls for tools that can display the full contract (see Viewing Contracts).
The other approach is to require developers to maintain the correct relationships themselves, by copying assertions as necessary. In this case, you see the full contract in each class, but need to verify for yourself that the inheritance rules hold in each case.
A runtime assertion-checking tool needs to be aware of the inherited contracts in either approach. In the first one, it must be aware of the full contract in order to check it properly. This is less of an issue for postconditions, because these can be checked independently. But it is crucial for preconditions, since the fact that a precondition written with a certain overriding method is false is not an error if the precondition of the method in some superclass is satisfied. In the second approach, assertions can be checked for each class independently, but you also want to add checks that warn in case the inheritance rules are violated.
Another complicating factor is the need to keep "old" values, as in the example shown above: "count = old count + 1". An assertion-checking tool needs to keep the value of count on entry to the method in order to make sure that the value on exit is exactly one more. The computation of the old value could cause an exception; for example, the Stack.put(x) method may have a postcondition "count > 1 implies item(2) = old item(1)", which means that if after insertion the stack has at least two elements, then the element that used to be on top of the stack ("old item(1)") is now second ("item(2)"). As the tool evaluates the expression item(1) on entry to the method, an exception will occur if the stack is empty. This, however, is not an error in the program or in the assertions! What the tool must do is keep a record of the exception, and only raise it if the old value is ever referenced. This will never happen in this example, because if the stack was initially empty, it will have exactly one element following the put operation, the antecedent "count > 1" of the postcondition will be false, and the consequent that calls for the old value, "item(2) = old item(1)", will never be evaluated.
Yet another challenge is preventing infinite recursion in assertion checking. For example, if the Stack class has an invariant capacity() >= 0, and this invariant is checked upon entry to the capacity method, an infinite recursion will result. Eiffel takes a conservative view, and turns off all assertion checking while it checks any assertion. The rationale for this is that methods used in assertions should be verified separately, before being used to specify the behavior of other parts of the program. It is possible to check more assertions without getting into an infinite recursion, and various tools have different strategies for dealing with this issue.
In order to get these kinds of warnings, the contract must be written in some formal language; typically it is some extension of boolean-valued expressions in the programming language. Extensions are necessary in order to refer to "old" values; that is, allowing the postcondition to refer to values of variables or expressions on entry to the method. Rich assertion languages also add ways of expressing universal and existential quantifiers; often, you want to express some property of all the contents of some array or other data structure, or to assert that there is some element with a given property. However, it is easy to add such capabilities to languages that don't offer them simply by writing appropriate methods to check any required condition.
Runtime contract checking is complicated by several factors. First is the relationship between contracts on methods in inheritance hierarchies. As I explained in The Correct Use of Inheritance, inheriting methods may only weaken preconditions and strengthen postconditions. There are two approaches to enforcing this constraint. The one used by Eiffel uses syntactic enforcement, in which you can never write assertions violating this rule. In Eiffel, overriding methods use "ensure then" instead of "ensure" for postconditions, with the meaning that any assertion following "ensure then" is conjoined with any assertions from superclasses. For example, suppose you have a postcondition "ensure contains(x)" on the method Container.put(x); this ensures that after adding x to the container, it is indeed in it. A list is a kind of container, and there you want to add the postcondition "ensure then count = old count + 1", which means that the number of elements in the list has increased by one after adding an element. (This is not the case for sets, which are also containers.) The full postcondition of List.put(x) is now "contains(x) and then count = old count + 1". (The "and then" operator is a short-circuit operator, like && in the C family.) Similarly, preconditions in overriding methods use "require else" rather than "require", and add the new assertions by disjunction ("or else" in Eiffel, || in the C family.) While this approach prevents any violations of the inheritance rules, it means that you don't see the full contract in the class itself, and there could be hidden assertions you aren't aware of. This calls for tools that can display the full contract (see Viewing Contracts).
The other approach is to require developers to maintain the correct relationships themselves, by copying assertions as necessary. In this case, you see the full contract in each class, but need to verify for yourself that the inheritance rules hold in each case.
A runtime assertion-checking tool needs to be aware of the inherited contracts in either approach. In the first one, it must be aware of the full contract in order to check it properly. This is less of an issue for postconditions, because these can be checked independently. But it is crucial for preconditions, since the fact that a precondition written with a certain overriding method is false is not an error if the precondition of the method in some superclass is satisfied. In the second approach, assertions can be checked for each class independently, but you also want to add checks that warn in case the inheritance rules are violated.
Another complicating factor is the need to keep "old" values, as in the example shown above: "count = old count + 1". An assertion-checking tool needs to keep the value of count on entry to the method in order to make sure that the value on exit is exactly one more. The computation of the old value could cause an exception; for example, the Stack.put(x) method may have a postcondition "count > 1 implies item(2) = old item(1)", which means that if after insertion the stack has at least two elements, then the element that used to be on top of the stack ("old item(1)") is now second ("item(2)"). As the tool evaluates the expression item(1) on entry to the method, an exception will occur if the stack is empty. This, however, is not an error in the program or in the assertions! What the tool must do is keep a record of the exception, and only raise it if the old value is ever referenced. This will never happen in this example, because if the stack was initially empty, it will have exactly one element following the put operation, the antecedent "count > 1" of the postcondition will be false, and the consequent that calls for the old value, "item(2) = old item(1)", will never be evaluated.
Yet another challenge is preventing infinite recursion in assertion checking. For example, if the Stack class has an invariant capacity() >= 0, and this invariant is checked upon entry to the capacity method, an infinite recursion will result. Eiffel takes a conservative view, and turns off all assertion checking while it checks any assertion. The rationale for this is that methods used in assertions should be verified separately, before being used to specify the behavior of other parts of the program. It is possible to check more assertions without getting into an infinite recursion, and various tools have different strategies for dealing with this issue.
Our own assertion-checking tool for Java is called Jose, and uses aspect-oriented programming to handle the issues mentioned above. More on Jose in a later post!
Tuesday, September 13, 2011
Viewing Contracts
There are many ways to view a contract. As you edit your class, you naturally see the contract for each method in the header of the method, since it is part of the text. (In Eiffel, it is part of the syntax of the language; in design-by-contract tools for Java, the contract is typically expressed in the Javadoc comments of classes and methods.) But what do you do when you want to see the full contract of the method, including inherited parts? And when you are about to call some method of another class, how do you know what is the full contact for that method?
The contract can have private parts, which are not meaningful to clients. As I mentioned previously, preconditions must be fully understandable to clients, since otherwise they can't be expected to comply with them. However, postconditions and invariants may refer to private parts of the implementation. For example, suppose your Queue class uses a circular buffer, called items, to implement the (bounded) queue. The implementation invariant should specify that items != null. This is an important property of the class to its implementer and anyone creating an inheriting class, but it shouldn't be exposed to clients who aren't supposed to know about implementation details. Similarly, the method that adds an element to the queue may have a postcondition specifying that the write-pointer has advanced by 1 modulo the buffer size. Again, this is useful information to the implementer, but should be hidden from clients.
The distinction between private and public access levels, in code or in contracts, is usually not clear cut. For example, Java provides four different access levels; C++ has three, but allows "friends" to access private and protected members; and Eiffel has a selective export mechanism where a class can specify which other classes have access to each of its features. This means that the same class can present different interfaces to other classes, depending on their access levels. As always, interfaces include the methods visible to the particular client, as well as the contract. Different parts of the postconditions and invariants of the contract would be visible to different clients.
The Eiffel development environment can show contracts in different ways, based on whether to show private assertions or not, and whether to show assertions from a single class or show all inherited assertions as well. However, the views can't be customized by client. I don't know of any other development environment that can show more than one view of a contract. Ideally, the environment would be able to show contracts from a supplier's perspective as well as from the perspective of any client. For example, if I'm editing class A, and place the cursor on a call to a method f of class B, I should be able to see (in a tooltip or in a separate view) the full contract of B.f from the point of view of class A. Such a view is not difficult to create, although it might require some tedious work. I consider such a view essential to any development environment that takes design-by-contract seriously.
The contract can have private parts, which are not meaningful to clients. As I mentioned previously, preconditions must be fully understandable to clients, since otherwise they can't be expected to comply with them. However, postconditions and invariants may refer to private parts of the implementation. For example, suppose your Queue class uses a circular buffer, called items, to implement the (bounded) queue. The implementation invariant should specify that items != null. This is an important property of the class to its implementer and anyone creating an inheriting class, but it shouldn't be exposed to clients who aren't supposed to know about implementation details. Similarly, the method that adds an element to the queue may have a postcondition specifying that the write-pointer has advanced by 1 modulo the buffer size. Again, this is useful information to the implementer, but should be hidden from clients.
The distinction between private and public access levels, in code or in contracts, is usually not clear cut. For example, Java provides four different access levels; C++ has three, but allows "friends" to access private and protected members; and Eiffel has a selective export mechanism where a class can specify which other classes have access to each of its features. This means that the same class can present different interfaces to other classes, depending on their access levels. As always, interfaces include the methods visible to the particular client, as well as the contract. Different parts of the postconditions and invariants of the contract would be visible to different clients.
The Eiffel development environment can show contracts in different ways, based on whether to show private assertions or not, and whether to show assertions from a single class or show all inherited assertions as well. However, the views can't be customized by client. I don't know of any other development environment that can show more than one view of a contract. Ideally, the environment would be able to show contracts from a supplier's perspective as well as from the perspective of any client. For example, if I'm editing class A, and place the cursor on a call to a method f of class B, I should be able to see (in a tooltip or in a separate view) the full contract of B.f from the point of view of class A. Such a view is not difficult to create, although it might require some tedious work. I consider such a view essential to any development environment that takes design-by-contract seriously.
Friday, August 5, 2011
Why Don't YOU Use Design by Contract?
I spent quite some time explaining why I think design by contract is such a useful methodology, and why every programmer (and especially those using object-oriented programming) should use it. So why isn't it more widely used?
One obvious reason is the lack of education about design by contract. You can find many university-level courses on object-oriented programming that hardly mention design by contract, or not at all. Naturally, people who don't know about it aren't going to use it. But the problem is deeper; I know that many of my own students don't use design by contract regularly. It may be that I wasn't able to convince them that it is really useful, but I know I'm not alone in this situation. I believe that for any methodology to be practically useful, it must be supported by tools that make its application not only painless but also rewarding. And tools for design by contract are not yet at a level that promotes wide use. Also, since programmers need instant gratification (see Programmers as Children), they need tools that will help them immediately, not only in some unforseeable (yet inevitable) future where the bugs they are writing now will come back to bite them.
I firmly believe that design by contract is useful even without any tools. But consider the amount of work required to use it. First, you have to write contracts for your methods. As I said before, these don't have to be complete, but they do have to capture those parts of the contract that are likely to cause the most trouble. Often, these are not deep properties of the computation. On the contrary, these are things like whether an argument to a method is allowed to be null, whether a string or an array may have zero length, whether a number has to be positive, and so on. This is not difficult to write, but write it you must. Then, you need to look at this contract whenever you call a method, and consider whether your call is correct. (Of course, if you don't bother, you will find your mistakes the hard way.)
But the most painful part of almost all methodologies arises when something needs to change. Whether your requirements have changed, or whether you are just refactoring your code, you will need to change some contracts, and create new ones. Then you will have to check whether all existing calls obey the new contract (or find out the hard way, as is the normal custom).
There are many opportunities for automated tools to help, at different levels . To begin with, you will want syntax checking of your contracts. This includes checking whether all preconditions can be evaluated by every caller; this is a must, since you can't require callers to obey conditions they can't evaluate. You may also want your IDE to provide syntax highlighting for contracts in a similar way to what it does with code. A "contract view" that shows just the contracts for a class would be nice, especially if it could show various views of the contracts. Such views can include the client view (where internal parts of the contracts are hidden), and the provider view (where everything is shown). The views would be customizable, for example, by showing only direct class members or all members (including inherited ones).
At a more semantic level, you want your contracts to be checked while you run the program. This has great value in catching errors close to their sources. However, instrumenting a program to check contracts is not trivial. Beyond the difficulties of instrumenting any program, the semantics of the contract require some tricky handling. One example is the ability to reference in postconditions the value of expressions on entry to the method, which requires tools to compute this value and keep it intact until the end of the call.
More generally, you may want a tool to help you verify the correctness of your implementation based on the contracts you write. Software verification is still an open problem, but it is a very active area of research. Part of this research is focused on verification of contracts.
Turning now to the question of change, tools that help manage contracts when the code changes are a must. Such tools can modify existing contracts as well as create (draft) contracts for new code. This is by no means easy. Analyzing an arbitrary piece of code to extract contracts from it is an open (and very difficult) problem, although some progress has been made on it. The problem is easier if some information about the intent of the change is available. This can happen in refactoring; when you invoke a refactoring such as Extract Superclass, the tool has an indication of your intent, and can use this information together with existing contracts for the class (or classes) you are generalizing in order to create the contract for the new superclass.
Needless to say, the Eiffel programming environment has the best tools for design by contract, although much more is needed. In future posts, I will describe some of the work my students and I have done on creating tools for design by contract in Java.
One obvious reason is the lack of education about design by contract. You can find many university-level courses on object-oriented programming that hardly mention design by contract, or not at all. Naturally, people who don't know about it aren't going to use it. But the problem is deeper; I know that many of my own students don't use design by contract regularly. It may be that I wasn't able to convince them that it is really useful, but I know I'm not alone in this situation. I believe that for any methodology to be practically useful, it must be supported by tools that make its application not only painless but also rewarding. And tools for design by contract are not yet at a level that promotes wide use. Also, since programmers need instant gratification (see Programmers as Children), they need tools that will help them immediately, not only in some unforseeable (yet inevitable) future where the bugs they are writing now will come back to bite them.
I firmly believe that design by contract is useful even without any tools. But consider the amount of work required to use it. First, you have to write contracts for your methods. As I said before, these don't have to be complete, but they do have to capture those parts of the contract that are likely to cause the most trouble. Often, these are not deep properties of the computation. On the contrary, these are things like whether an argument to a method is allowed to be null, whether a string or an array may have zero length, whether a number has to be positive, and so on. This is not difficult to write, but write it you must. Then, you need to look at this contract whenever you call a method, and consider whether your call is correct. (Of course, if you don't bother, you will find your mistakes the hard way.)
But the most painful part of almost all methodologies arises when something needs to change. Whether your requirements have changed, or whether you are just refactoring your code, you will need to change some contracts, and create new ones. Then you will have to check whether all existing calls obey the new contract (or find out the hard way, as is the normal custom).
There are many opportunities for automated tools to help, at different levels . To begin with, you will want syntax checking of your contracts. This includes checking whether all preconditions can be evaluated by every caller; this is a must, since you can't require callers to obey conditions they can't evaluate. You may also want your IDE to provide syntax highlighting for contracts in a similar way to what it does with code. A "contract view" that shows just the contracts for a class would be nice, especially if it could show various views of the contracts. Such views can include the client view (where internal parts of the contracts are hidden), and the provider view (where everything is shown). The views would be customizable, for example, by showing only direct class members or all members (including inherited ones).
At a more semantic level, you want your contracts to be checked while you run the program. This has great value in catching errors close to their sources. However, instrumenting a program to check contracts is not trivial. Beyond the difficulties of instrumenting any program, the semantics of the contract require some tricky handling. One example is the ability to reference in postconditions the value of expressions on entry to the method, which requires tools to compute this value and keep it intact until the end of the call.
More generally, you may want a tool to help you verify the correctness of your implementation based on the contracts you write. Software verification is still an open problem, but it is a very active area of research. Part of this research is focused on verification of contracts.
Turning now to the question of change, tools that help manage contracts when the code changes are a must. Such tools can modify existing contracts as well as create (draft) contracts for new code. This is by no means easy. Analyzing an arbitrary piece of code to extract contracts from it is an open (and very difficult) problem, although some progress has been made on it. The problem is easier if some information about the intent of the change is available. This can happen in refactoring; when you invoke a refactoring such as Extract Superclass, the tool has an indication of your intent, and can use this information together with existing contracts for the class (or classes) you are generalizing in order to create the contract for the new superclass.
Needless to say, the Eiffel programming environment has the best tools for design by contract, although much more is needed. In future posts, I will describe some of the work my students and I have done on creating tools for design by contract in Java.
Wednesday, January 19, 2011
Training Wheels
Parents put training wheels on bicycles when children first start learning to ride. Once the children have enough experience and confidence, however, the training wheels get in the way and are removed. You will never see an experienced cyclist using training wheels. But you will often see experienced computer users, including software developers, using features meant for novices.
There is certainly a lot to learn about using computers, especially with complex applications such as development environments. It makes sense to help people learn and adjust to the proper use of these applications. But as they acquire experience, they should be able to remove the training wheels, lest they become a burden. And giving people help on the learning curve should not mean dumbing down interfaces!
The mouse is a case in point. I believe that taking the mouse away from developers' workstations will improve developer productivity more than any other action, possibly after a small adjustment period. I keep seeing developers waste their time using the mouse to hunt for a specific menu entry, which may be hidden under one or more menu levels. This is becoming more and more difficult as screen resolutions get higher and the clickable area gets proportionately smaller. Instead, a short sequence of keys (two to five keystrokes) will achieve the same result in less than a second.
Of course, in order to take advantage of the keyboard you need to memorize quite a few such sequences. This is impossible for a casual user of the application, but happens easily and automatically for an experienced user, given some learning time. It's just like riding a bicycle: at first you need to think explicitly about every action you take, but after a while it becomes automatic. And this is true for any repetitive activity, such as walking, swimming, driving, or touch typing: if you practice enough, it becomes automatic to the point where trying to think about it is actually confusing. Try to think about the mechanics of walking while you do it, and you will lose your step. I touch-type, and when I'm asked about the location of some key I need to mimic the typing movement in order to get reminded of the location through my muscle memory. Similarly, I know a great many keyboard shortcuts for applications I use frequently, but I find it hard to articulate the key sequences.
I'm an avid Emacs user. Emacs (The One True Editor) has many excellent qualities, but it has a steep learning curve because of its many keyboard shortcuts, which can use any combination of the Ctrl and Alt modifiers with any other key, and also have multi-key combinations. (The Lisp Machine keyboard, also called the space-cadet keyboard, had two additional modifiers, called Super and Hyper). Once you learn these sequences, however, you can make text fly on the screen. I remember sitting next to one expert when I first got to MIT; I was staring at the screen and seeing the program transform itself, and I couldn't relate what I was seeing to the keystrokes used to create that effect. I have since had that effect myself on other people.
For many years I taught courses based on the excellent book Structure and Interpretation of Computer Programs,
which uses the Scheme programming language. My interpreter of choice for that course was MIT Scheme, whose editor component is based on Emacs. However, students prefered to use Dr. Scheme, simply because it has a more conventional editor. I can understand their reluctance to learn the Emacs keystrokes, since they thought this skill would only be useful in that single course. But there is no justification for professional developers to avoid learning skills that will make their work easier and faster.
Unfortunately, the tendency among tool developers is just the opposite. They target their interfaces at the lowest common denominator, thus making the tools attractive to the novice user but hampering the experienced user. Mouse-based interfaces are today much preferred over keyboard shortcuts, to the extent that Microsoft has eliminated the indications of which keyboard shortcuts are available (using underlined characters); these can be revealed through the "accessibility options" dialog, as though you are somehow mouse-challenged if you want to use the keyboard. (Hint: look for "Show extra keyboard help in programs".)
Because of this behavior of tool writers, users have no chance to learn the keyboard shortcuts and make their use automatic. Thus, they are stuck with the laborious use of the mouse and never outgrow their training wheels. As always, you should use the right tool for the job. The mouse is good for applications you only use occasionally, and is indispensable for some graphics applications. But keep your hands on the keyboard for software development!
There is certainly a lot to learn about using computers, especially with complex applications such as development environments. It makes sense to help people learn and adjust to the proper use of these applications. But as they acquire experience, they should be able to remove the training wheels, lest they become a burden. And giving people help on the learning curve should not mean dumbing down interfaces!
The mouse is a case in point. I believe that taking the mouse away from developers' workstations will improve developer productivity more than any other action, possibly after a small adjustment period. I keep seeing developers waste their time using the mouse to hunt for a specific menu entry, which may be hidden under one or more menu levels. This is becoming more and more difficult as screen resolutions get higher and the clickable area gets proportionately smaller. Instead, a short sequence of keys (two to five keystrokes) will achieve the same result in less than a second.
Of course, in order to take advantage of the keyboard you need to memorize quite a few such sequences. This is impossible for a casual user of the application, but happens easily and automatically for an experienced user, given some learning time. It's just like riding a bicycle: at first you need to think explicitly about every action you take, but after a while it becomes automatic. And this is true for any repetitive activity, such as walking, swimming, driving, or touch typing: if you practice enough, it becomes automatic to the point where trying to think about it is actually confusing. Try to think about the mechanics of walking while you do it, and you will lose your step. I touch-type, and when I'm asked about the location of some key I need to mimic the typing movement in order to get reminded of the location through my muscle memory. Similarly, I know a great many keyboard shortcuts for applications I use frequently, but I find it hard to articulate the key sequences.
I'm an avid Emacs user. Emacs (The One True Editor) has many excellent qualities, but it has a steep learning curve because of its many keyboard shortcuts, which can use any combination of the Ctrl and Alt modifiers with any other key, and also have multi-key combinations. (The Lisp Machine keyboard, also called the space-cadet keyboard, had two additional modifiers, called Super and Hyper). Once you learn these sequences, however, you can make text fly on the screen. I remember sitting next to one expert when I first got to MIT; I was staring at the screen and seeing the program transform itself, and I couldn't relate what I was seeing to the keystrokes used to create that effect. I have since had that effect myself on other people.
For many years I taught courses based on the excellent book Structure and Interpretation of Computer Programs,
Unfortunately, the tendency among tool developers is just the opposite. They target their interfaces at the lowest common denominator, thus making the tools attractive to the novice user but hampering the experienced user. Mouse-based interfaces are today much preferred over keyboard shortcuts, to the extent that Microsoft has eliminated the indications of which keyboard shortcuts are available (using underlined characters); these can be revealed through the "accessibility options" dialog, as though you are somehow mouse-challenged if you want to use the keyboard. (Hint: look for "Show extra keyboard help in programs".)
Because of this behavior of tool writers, users have no chance to learn the keyboard shortcuts and make their use automatic. Thus, they are stuck with the laborious use of the mouse and never outgrow their training wheels. As always, you should use the right tool for the job. The mouse is good for applications you only use occasionally, and is indispensable for some graphics applications. But keep your hands on the keyboard for software development!
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.
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
Subscribe to:
Posts (Atom)