Larry Latour
Associate Prof, Dept. of CS
| Monday, 8/31 | Introduction |
| Wednesday, 9/2 | Parnas/Lamport Overview |
| Friday, 9/4 | Clocks and "Happened Before" |
| Wednesday, 9/9 | 3 Aspects of Mutual Exclusion algorithm |
| Friday, 9/11 | 3 Aspects of Mutual Excl usion algorithm (cont.) |
| Monday, 9/14 | 3 aspects of Mutual Exclusion algorithm (cont.) |
| Wednesday, 9/16 | Mutual Exclusion Algorithm Proof of Correctness |
| Friday, 9/18 | The Inevitable Intertwining of Specification and Implementation |
| Monday, 9/21 | The Criteria for Decomposing Systems into Modules |
| Wednesday, 9/23 | The Criteria for Decomposing Systems into Modules (cont.) |
| Friday, 9/25 | Why Black Boxes are so Hard to Reuse |
| Monday, 9/28 | Black Boxes, Software Design, and Conceptual Models |
| Wednesday, 9/30 | Using Documentation as a Software Design Medium |
| Friday, 10/2 | A Review of Lamport Implementation - Prototyping? |
| Monday, 10/5 | A Review of Lamport Implementation (cont.) |
| Wednesday, 10/7 | Building Interactive Characters |
| Friday, 10/9 | Building Interactive Characters (review) |
| Wednesday, 10/14 | Using Documentation as a Software Design Medium (cont.) |
| Friday, 10/16 | A Rational Design Process: How and Why to Fake It |
| Monday, 10/19 | A Rational Design Process: How and Why to Fake It (cont.) |
| Wednesday, 10/21 | Comparing Architectural Design Styles |
| Friday, 10/23 | Comparing Architectural Design Styles (cont.) |
| Monday, 10/26 | Comparing Architectural Design Styles (cont.) |
| Wednesday, 10/28 | The 4+1 Model of Architecture |
| Friday, 10/30 | A Framework for Software Development |
| Wednesday, 11/4 | Architectures vs. Components, Documentation Goals, and Paper Writing |
| Friday, 11/6 | The Goals of Formal System Specification |
| Monday, 11/9 | Two-tiered Formal Specification (Trait Specifications) |
| Wednesday, 11/11 | Two-tiered Formal Specification (Trait Specifications - cont.) |
| Friday, 11/13 | Two-tiered Formal Specification (Trait Specifications - cont.) |
| Monday, 11/16 | Two-tiered Formal Specification (Interface Specifications) |
| Wednesday, 11/18 | Two-tiered Formal Specification (Interface Specifications - cont.) |
| Friday, 11/20 | Two-tiered Formal Specification (Interface Specifications - cont.) |
| Monday, 11/23 | The Larch Family of Specification Languages |
| Wednesday, 11/25 | Formal verification partial correctness and temination |
| Friday, 11/27 | Formal verification sequential reasoning |
| Monday, 11/30 | Formal verifcation - the nature of loop invariants |
| Wednesday, 12/2 | Formal verification - the nature of loop invariants (cont.) |
| Friday, 12/4 | Formal Verification - selection and loop invariant rules |
| Monday, 12/7 | Reasoning about Data Abstractions |
Assignment:
Assignment:
Assignment:
Assignment (part I):
Assignment (part II):
I digressed a bit with a discussion of the specification as contract between user and implementor, the nature of usage and implementation (front-end users vs. user modules in a layered system), user policy vs. implementation mechanism, and user requirements vs. implementor needs. We then talked about the requirements of the mutual exclusion algorithm and how these requirements were addressed with an implementation based on the particular total ordering of events previously discussed.
Assignment (part I):
We ended with an initial investigation of the algorithm implementation, to be continued next class.
Assignment (part II):
Assignment:
With respect to C1 and C2, we reviewed their initial description from the Friday, September 4 class. Recall that C1 and C2 stated what needed to happen for a clock to be correct (a specification), and IR1 and IR2 stated what needed to happen in order to insure C1 and C2 (an implementation). The verification, or proof , that IR1 and IR2 indeed satisfied C1 and C2 was simple, almost trivial, but still needed to be stated. Lamport does this.
We then broke into groups to try to understand Lamport's arguments that the mutual exclusion algorithm implementation satisfied the specification rules I to III. We realized that the argument was multi-stepped and non-trivial, and it took us a good deal of time to satisfy ourselves that the argument held.
I pointed out that these arguments, or proofs of algorithm correctness, need to be provided in good program documentation as a way of communicating implementation design decisions from person to person. In addition, this communication process involves more than just the text of the argument. It also involves a series of multi-viewpoint explanations through example and symbolic reasoning in order to satisfy each team member that the argument is correct.
Assignment:
Assignment:
Looking first at Line Storage, we identified the following issues:
Assignment:
The issue of coupling arose. I raised the the question of whether the Line Storage and Circular Shift implementations were completely independent of one another, or whether they might be dependent on one another in some way. Certainly the efficiency of the Line Storage operations both individually and with certain patterns of use will be a factor in how the Circular Shifter operations are implemented. In fact, efficiency tends to be a global concern that is not easily decomposed into modules in a large system .
Lead in to Next Class:
In a related issue, Parnas pointed out that the Circular Shifter CSSETUP could be either doing work or not, depending on the implementation. This "when is the actual execution time?" issue is actually an extremely important one in complex, layered systems. Caching is one example. Pre-fetching of instructions is another. Compilation vs. Interpretation is actually another.
Assignment:
Today we looked at the Gregor Kiczales video "Why Black Boxes are so Hard to Reuse: A New Approach to Abstraction for the Engineering of Software" (Click here for his Open Implementation Group website). To me the essence of his "thesis" was stated fairly late in the video (I'm paraphrasing) - "Interfaces are partial descriptions! What's really going on is much too complex to specify." The problem with partial descriptions is that a good number of implementation design decisions that aren't communicated in the interface are the wrong ones for certain applications. It seems to me that while most of Kiczales' points are also arrived at by a good software engineer with common sense, he provides a useful framework and language for thinking about and communicating these points.
Here are a list of issues in his videotape that are worth thinking about further:
Related papers referenced in the video:
Assignment:
We gave a short summary of essentially what I summarized in the posting for last class , with a slightly different view of what's really going on than that posted. Students pointed out that the point of the paper was to build software that can be reused across many contexts, all sharing a common base specification and its associated implementation, but each requiring a different meta specification and its associated implementation. It is certainly true (as I said) that interface specifications only contain part of the information needed to implement a user program, but the paper's focus is on solutions to this problem.
We next discussed Mitchell Kapor's "A Software Design Manifesto" paper. His major point was that we need to treat software design as a separate and distinct profession from computer science and software engineering. He emphasized that software design is not just user interface design, but (as Liddle points out in the next paper) that it is concerned with the design of the entire user conceptual model , encompassing the complete user experience.
Kapur distinguishes software design from computer science and software engineering as follows:
The Kapor paper led us nicely into the David Liddle's "Design of the Conceptual Model" paper. Liddle's focus is on the design of the user conceptual model , and he uses the Xerox Parc Star system as his case study. Three principles central to the Star design are (1) direct manipulation, (2) a WYSIWYG interface, and (3) command consistency. The conceptual model development was a reflective one, leading to a 400 page design document before 1 line of code was generated.
A number of factors seemed to doom the success of Star as a product (although valuable lessons were learned from it as an R&D effort):
Assignment:
Our discussion approach was to divide the Hester design documents among the class, also assigning the "meta", or overall view as a document. Before we began I pointed out the author's comments about the need for precision in pictures and diagrams. We agreed that arrows in the meta-diagram defined a dependency relationship, and not necessarily a strict "happened before" relationship.
We spent the remainder of the class focused on the Requirements Specification document, partly because we discussed properties generic to all documents, and partly because the Requirements Document is fairly extensive. Specifically, we looked at the Scope/Use/Design Considerations structure of each document, the general principles for writing each document, and the stylistic rules for each document. We didn't discuss review procedures and justification data for document design decisions.
We wrapped up the discussion with an overview of the specific scope, use, and design considerations for the Requirements Specification document. I asked why the authors specifically used the term "Design" in "Design Considerations", and I also wanted to know how Liddle's User Conceptual Model fit within Hester's framework. It seemed to belong in the Requirements Specification, but where specifically? Were there issues of concern to Liddle that weren't addressed by Hester? I'll leave this for an assignment.
Upcoming work/events:
I observed that given some time we could develop an integrated prototype that combined the best features of each of the projects, allowing us to observe system behavior from many viewpoints. I also noted that prototypes have an important place in documentation frameworks, playing an important role not only in validating requirements but in communicating specifications to both the user and implementor. At very least they liven up the conversation between software engineers.
Assignment:
Prof. Hayes-Roth began by outlining the "complete AI problem", specifically the problem of how perception and action act as an interface between cognition and the environment. She then used this framework as a context for describing the complex, intelligent web agents of her research.
A number of application domains were posed as targets for intelligent agent usage, including site planning, medical monitoring systems, office robots, errand planning, etc. Developing general domain requirements and analyzing how these requirements are constrained across domains is also an important issue in software engineering requirements analysis. Prof. Hayes-Roth's paper "A Domain-Specific Software Architecture for Adaptive Intelligent Systems", in our readings packet, specifically sets her work in software engineering context.
In both Prof. Hayes-Roth's talk and her DSSA paper a diagram is presented describing the three separable dimensions of her software systems. These dimensions, tasks, methods, and domain knowledge correspond roughly to specifications, implementations, and Kizcales's meta-knowledge. Furthermore her goal is to design these pieces so that they can be composed in a plug-and-play style, and this is where reuse comes in . A good deal of up-front architectural analysis is needed here, since we tend to design for a particularly problem at hand, mixing domain-independent design decisions in the specification and implementation with the domain-dependent design decisions peculiar to our specific domain of study.
Prof. Hayes-Roth's slide on the value of decomposition should have touched a familiar chord with us. She points to three advantages to decomposition: cognitive flexibility, incremental development, and reuse. This is exactly what Parnas was talking about in his Decomposition paper. While this is not at all a new idea, it is important to restate if we're interested in capturing component architectures formally. Her three part separation, stated also by many other Systematic Reuse researchers, is very important to the DARPA Domain Specific Software Architecture effort, and very important to the Systematic Software Reuse community.
Assignment:
I asked the class how they approached the overall KWIC index implementation. Specifically, did they think "globally" or "locally" when implementing each layer? One student commented that it was actually a good deal more difficult and frustrating to implement in layers than it would have been in a single unit with all design decisions visible. I brought up an alternative example of the BRAIN Operating System implemented in our COS431 class. The modular decomposition is more "accepted" with this system simply because the implementation models at each system layer are fairly complex and vastly different from the other layers. The dynamics of process execution in the kernal are much different than at the application layer.
Assignment:
I pointed out that in the waterfall model there are typically separate requirements gathering/formalization and specification phases. Hester, et al. combines these steps into one Requirements Specification Document. This might have something to do with the maturity of applications that his group is working on, or simply that Hester considered the system specification to be the output of the Requirements Analysis phase. We also noted that Liddle's User Conceptual Model is included in the Requirements Specification document if it is included anywhere.
Before discussing the remaining documents in the document schema, we discussed the role of rationale/justification information in each document. Hester does discuss where this information should appear (in section 6.5). The issue seems to be one of unwieldyness. Justification documents should accompany but be separate from all documents except the module interface and module design documents. While the paper doesn't explain these exceptions, it would seem to me that in both of these cases this justification information would be extremely important for the implementor in order to confidently carry out the designer's wishes, possibly more so than in the other documents.
We continued with a discussion of the Module Decomposition, Module Interface, Module Design, and Module Dependency Documents.
But how and in what order are all of these documents composed? Clearly there's a chicken/egg problem here. In fact the documents are to an extent co-dependent. You can't decompose a system into modules unless you have some sense of the internal implementability (module design) of each module. You can't nail down the specifics of module design unless you have a sense of the services provided by other modules (module interfaces). And you certainly can't define the module dependencies until you've thought a good deal about the other three documents.
This co-dependence of document development is addressed nicely in the Parnas, David L. and Clements, Paul C. "A Rational Design Process: How and Why to Fake It" paper, which we will discuss next class.
Assignment:
Each student took one of the points and illustrated it with examples from experience.
Examples included:
I pointed out that the relative maturity or immaturity of an application domain can have a large impact over how close an approximation we can get to the rational design process. When the technological domain, for example, is rapidly changing (e.g., helicoptor control susyems), then a good deal of domain information has to be redeveloped or developed from scratch. This leads to a process that is highly parallel and exploratory rather that rational and sequential. But more on this in the next class session.
Our discussions led us to some interesting points and examples:
In summary, there IS an ideal, sequential process of developing design documentation, but in reality it is almost impossible to achieve except in the most mature of software domains (yet another payroll program for example). The reality of incomplete information, human limitations, and the co-dependence of many design decisions leads us to develop a set of seemingly time-ordered documents in a highly parallel, feedback oriented style.
Assignment:
I pointed out that an important part of the paper is the comparison section beginning on page 36. It is important for us to apply the range of evaluation criteria to an architectural design style, and we will do this as part of our final class project. We will discuss these criteria in subsequent classes.
We first discussed the user conceptual model of the cruise control problem , pointing out that it is similar, but not exactly the same as, the cruise control model we are all familiar with (or most of us at least).
Assignment (part):
Looking at Booch's dataflow, functional, and object-oriented diagrams, we noted that they all were made up of circles and arrows, but that these circles and arrows had a very different meaning in each case. In the object-oriented diagram the circles were objects and the arrows defined a "uses", or "dependency" relationship based only on the "actions each object suffers and that it requires of other objects". Although it is tempting for us to read more into the arrows than they actually convey, it is dangerous to do so. For example, these access arrows say very little about the flow of data or the underlying process architecture and their communication protocols. In fact, defining the arrows without knowing something about dataflow and process structure views is probably premature. The arrows after all define the delivery mechanism between the objects, but not necessarily what is being delivered or picked up.
In the case of the functional design vs. the object-oriented design, I pointed out that I often use a combination of functional and object-oriented decomposition to form module interfaces and associated (internal) design.
We proceeded to Yin and Tanik's object-oriented design and compared their design with Booch's. We found that with the little information we knew about the architectures we still could hypothesize a mapping between the two designs. It turned out that the primary difference between the two was that Yin and Tanik encapsulated the Cruise-Control System inside a module whereas Booch didn't. Given Parnas's reasons why information should be hidden inside of a module/object, it seemed to us that there were design decisions concerning the internal design of the cruise control system that must be dealt with and changed in an integrated fashion, hence encapsulated.
Next class we will finish up with the object-oriented architectures and look carefully at the state-based architectures.
Assignment (part):
After a bit of confusion, we realized that figures 10-12 represented a three level architecture, where the top two levels (overall architecture and control_sp "subarchitecture") were very different from the bottom level (the speed control state chart). The top two levels defined the system entities along with the activities that used and affected them. There is a similarity between these activity charts and object diagrams, but they aren't exactly alike. In activity charts, the arrows encapsulate the functionality of the system (and are actually coded in Statemate). In object diagrams the arrows are meant to define what objects access what other objects, and the functionality is encapsulated in the objects themselves.
State charts represent an entirely different "animal". The boxes represent the possible states, or values, of the encapsulating entity, and the arrows represent state transitions, much like the state machine formalisms discussed in COS550 (Theoretical Computer Science I). So for one of these multi-tiered models to be "executable", all of the lowest level entities need to be "implemented" with state machines. We've already discussed the lack of a clear distinction between many specifications and their implementations, and the use of state diagrams to specify and prototype implementations further blurs the distinction. We will see this again during the formal specification portion of the class. We will use predicate calculus based specifications that actually can be "run" very much like Lisp programs. Are these entities both specification and implementation?
I asked how these diagrams would fit in the Hester documentation schema. It seems that aspects of the activity charts match up to the module decomposition, module interface, and module dependency documents, whereas the state diagrams fit into the module design document. The activity charts define higher level "architectural" aspects of a system, whereas the state diagrams represent a high level design view of a module implementation. Of course, state diagrams are only one view of a module design. Code is another. Others, like those we will see in the feedback-control and real-time architecrues, capture other aspects of a module design.
A common thread in all of these architectural diagrams is that each has a "formal" syntax and semantics all its own, and that a clear understanding of this syntax and semantics is needed in order to properly interpret a diagram and compare it to other formalisms. For example, it was a bit unclear at first what the boxes and arrows of the Statemate state diagrams were, especially since they were used along with earlier diagrams that had a very different semantics for boxes and arrows. I put "formal" in quotes because while (1) we don't normally think of diagrams as having a formal syntax and semantics, (2) there's nothing that prevents them from having such a definition. In fact, this is what allows us to create executable Statemate diagrams (much like Stella models for those of you familiar with the Stella dynamic system modeling language and environment).
Assignment:
In the Higgins method, a standard feedback-control model template was added to the already useful Data Structured System Development method for real-time embedded systems. A feedback entity diagram is derived from a feedback control diagram by capturing the feedback loop in a "summing point" entity, comparing actual and desired loop values.
The Shaw method improves on the Higgins method by factoring out the engine control problem from the engine activity/inactivity problem. It also captures the mapping from analog to digital control with the clock and associated event table. This seems to provide the cleanest "separation of concerns" for the cruise control problem.
Shaw posed an important checklist for when to consider a control-loop design:
We also discussed where the object oriented model fits in. From an OO standpoint recognizing that a system has a control loop architecture is important from the perspective of encapsulation. The collective parts of the control loop are dealt with as an integrated set of design decisions, which need to be encapsulated if this control loop model changes. Shaw's analysis further allows us to separate the activity/inactivity state machine from the control unit, allowing us to separate concerns at a more granular level and giving us a bit more design flexibility.
Assignment:
The five views in the model are:
The point was made that these views weren't separate system components, but alternative viewpoints on the overall system. Any integrative views therefore describe how these views complement each other, not how they fit together as components. In that sense the integrative views are "meta"-views. The scenarios for example are an attempt to capture this meta-information with respect to one or more case-studies of systems or system domains. I pointed out that it was crucial for us to formalize this meta-information, not only to raise the software engineer's sense of awareness of it's existence, but to reason and make decisions about the proper integration of the base views. Kruchten talks about the use of formal scenario scripts , sequences of interactions between objects and between processes. He points out that these scenarios are "in some sense an abstraction of the most important requirements".
Assignment:
The class broke into two groups to discuss the overall framework, then sent envoys to the other group to report their views and blend them with the views of the other group. We haven't yet discussed these views, but we will when we review the in-class exam to be taken on Monday, November 2, 1998. The following question will be on the test:
In comparing the Wheeler paper with the Hester paper, we saw that the primary difference was that Wheeler was trying to develop documentation as a complete, verifiable picture of the system being described. In doing so, he modified Hester's documentation schema to include the following features:
In comparing Kiczales's videotape and online papers to Garlen's paper, we saw that the primary difference was one of focus (my distance glasses vs. reading glasses example). Garlen focused on the reuse problem from the architectural viewpoint whereas Kiczales focused on the reuse problem from the component viewpoint.
While the focus was different, this doesn't mean that the ideas didn't overlap a great deal. In fact, architectural concerns were critical to the Kiczales meta-object protocol concept, and component concerns were critical to the Garlen architectural mismatch issue. The papers are complementary and need to be taken as a whole in order to deal with the overall reuse problem.
One student raised the issue of design with reuse vs. design for reuse. Design with reuse is concerned with the construction of systems using reusable artifacts, and design for reuse is concerned with the construction of the reusable artifacts themselves (These terms, by the way, have become an accepted part of the reuse community's lexicon). While Kiczales was concerned primarily with design for reuse, he also motivated his discussion with examples showing poor design with reuse (mapping conflicts due to poor mapping decisions). While Garlen was motivating his paper with the architectural mismatch due to poor design with reuse, he also suggested a number of criteria for designing both components and architectures for reuse. It turns out that most reuse papers I would point the class to have design for reuse as their primary goal, with design with reuse as a long term outcome of the paper results.
In going over the question answers/papers, I noticed a pattern emerging.
Today I used Tom Wheeler's Software System Development Through The Use of Formal Documentation paper to motivate the upcoming discussions on formal specification. I again described the three tiers of Wheeler's documentation schema, noting that he was concerned with building a completely verifiable system from user requirements to code.
The goal of formally proving the correctness of all aspects of a system is a noble one, but is probably not realistic due to the inadequacy of tools to describe the system constraints. For example, we will spend time looking at the semantics of I/O functionality (e.g., a function taking inputs x and y yields output z), but we'll gloss over methods of formalizing the so-called "non-functional" constraints - time and space. This doesn't mean that work hasn't been done in these areas - real time systems developers for example have taken advantage of research formalizing the concept of time. It does mean that these techniques have not been well integrated into the software engineering culture. Even functional specification methods are generally viewed with distaste by most practicing software engineers.
Despite these drawbacks, formally specifying those aspects of a system that we can is still a useful exercise. It not only gives us a better collective understanding of what the system does, but it gives us a limited ability to reason formally about system behavior.
Today we began to look at the process of formal specification, concentrating on the two-tier specification techniques from Barbara Liskov's and John Guttag's book, Abstraction and Specification in Program Development. The bottom tier, trait specifications described algebraically, are used to model value semantics, whereas the top tier, model based interface specifications, are used to model object semantics.
To motivate the difference between value and object semantics I compare pure Lisp, a functional or imperative language, to C++, a procedural or applicative language. Pure Lisp abstracts away the notion of an object along with all of the design decisions that must be made concerning object manipulation and storage. Note that pure Lisp doesn't even have a notion of assignment! Recall also that Gregor Kiczales mentioned in his video that Lisp handles the mapping dilemna problem by making believe it isn't there. This is what he means. Lisp program object optimization is done by the compiler implementor. C++ on the other hand deals with both object and value semantics, and the programmer has to be explicit about object manipulation and storage.
The primary purpose of this two-tier separation of value and object semantics is to allow the specifier to model value semantics cleanly, and to allow the specifier to use any one of many language dependent interface specification languages. The language used in the paper you are reading is based on the CLU (for CLUSTER) language developed by Liskov and Guttag, but there are also interface specification languages targeted to languages such as C++ and the Department of Defense's Ada programming language.
A few words about model based vs. algebraic specification: the original Goguen, Thatcher, and Wright papers on algebraic specifications used a purely algebraic style to describe program specifications. As we've seen, the Liskov and Guttag work uses algebraic specifications as a way to model the value semantics of an interface, but uses a more traditional pre and post condition interface specification to describe interfaces tailored to traditional applicative programming languages. We will also look at a third approach, the Z (pronounced "Zed") specification group originating at Oxford. Rather than write algebraic specifications from scratch for every new interface, they point the specification writer to mathematics texts for a few basic well-defined models, e.g., sets, sequences, trees, graphs. Model based specifications are then written using these well known models. In a similar approach, Jeannette Wing (then of MIT, now of CMU) followed up the Liskov and Guttag work by organizing her algebraic models into an inheritance hierarchy for ease of use.
Another consequence of the two-tier approach is that one can write the interface specification or the algebraic model first, leaving the details of the other for later.
We began to look at algebraic trait specifications through the STRING_TABLE specification. We first saw that each STRING_TABLE had a set of operations and a set of constraints. Since algebraic models describe values purely from an observer's viewpoint, the way to "name" values is by the sequence of operations that are performed on them. For example, ADD(ADD(NEW,'a',3),'b',4) describes a string table value with two elements added, and DELETE(ADD(NEW,'a',3),'a') describes a string table value with an element added, then deleted.
Since intuitively there are many different sequences of operations that can lead to the same string table value, this leads us to describe a single string table value with an equivalence class of these "term" names. Furthermore there is one special "canonical", or shortest, string table name in each equivalence class, the term made up of just ADD and NEW operations (with no DELETE operations). The constructor constraints, or axioms, are used to equate any pair of terms in an equivalence class, and specifically allow all terms to be equated to the canonical term. The observer constraints, or axioms, then allow us to observe properties of all string table names through just the canonical names.
We finished the class with a word about constructing algebraic models. This is a design process, in the sense that design is the human process of creating formal artifacts. I mentioned that the first thing a mathematician does is to abstract away all meaning into a formal system which can then be used to reason with. A similar process happens here. As software engineers we start out with an informal user conceptual model of a string table, usually model based in the sense that we have a "visual" model in our head of what a string table does. We then start defining algebraic properties of this model, properties that must always hold if the model is to be implemented correctly. For example, if we are writing a stack specification, a push followed by a pop must always cancel each other out, yielding the original stack. When we succeed in defining a complete "cover" of algebraic properties (more on this in a subsequent class), we can then throw away our conceptual model and reason mechanically in a purely formal, syntactic manner, using just our algebraic properties. If we are reasonably sure that these properties are not redundant then we call them axioms and build all of our theorems from them. We can even write an automatic theorem prover to reason about theorems relating to the specifications (more on these in a subsequent class).
The following assignment explores the construction of trait specifications and interface specifications, and is due when we spend a bit more time with the topic (sometime next week):
In this class we looked a bit deeper at the mechanics of trait specifications. The following topics were covered:
Terms and well-formed formulas: we talked specifically about how terms are built up from the specification, and how well-formed formulas, or assertions were crafted from these terms.
Equivalence classes of terms: we again discussed how terms group together into equivalence classes, and how there is one canonical term that is somehow the shortest or simplest onto which all other terms in the class can be mapped. We saw that the constructor constraints, or axioms, define what terms are equivalent to what others, and that these constraints act as rewrite rules for converting one term in a class to another.
Theorems, Proofs, and the Theory of a Trait: We saw that theorems are a subset of the well-form formulas that are true, proofs are the verification that well-formed formulas are correct through the repeated application of the trait axioms, and the theory of a trait is the set of all true well-formed formulas (theorems) of the trait. We also saw that well-formed formulas are composed of (1) all terms of sort bool, all equations of the form t1 = t2, where t1 and t2 are of the same sort, and all WFFs built from other WFFs and the universal and existential quantifiers (for all and there exists).
An example proof from the reading was given as a template. I noted that mathematicians are good at balancing informal ("outside of the formal system") with formal ("within the formal system") reasoning. Often students get "taken to task" by their math professor for either ignoring the formal system altogether and "proving" nothing, or staying completely within the formal system and blindly applying rules without any intuitive sense of where they're heading in the proof.
Assignment:
Prove:
(1) size(add(add(new,x,y),x,z)) = 1
let Z = add(delete(add(add(new,x1,v1),x2,v2),x1),x3,v3)
(2) is_in(Z,x1) = false
(3) size(Z) = 2
Today we began by discussing two important conceptual clauses in the trait: the generated by and partitioned by clauses.
The generated by clause defines the basic constructors that are used to generate all possible canonical terms of the type of the trait. This is extremely important when constructing inductive proofs of theorems containing the for all and there exists quantifiers. For example, the generated by clause in a stack trait contains [NEW,PUSH]. Depending on the operation mic of the trait specification, the trait designer might have a choice as to what to include in the genertaed by clause. That is, there might be more than one way to define all possible canonical terms in the trait.
The partitioned by clause defines the minimum set of operations that can detect an observable difference in two terms if there is one. It is very important to stress observable difference here. When we design an abstract data type, we craft exactly what we want the user to see in the interface. This is largely independent of implementation differences that might exist. For example, we might implement a set abstract data type with an unordered array containing duplicates. This would make inserts very efficient at the expense of searches and deletes. Two set implementations could contain a very different mix of the same elements that are in another set implementation, but from the point of view of the user these are still the same sets. This same concept holds with the partitioned by clause. A trait without observers is meaningless, and the [artitioned by operations define the user point of view.
I was careful to say "a minimal set of operations" and not "a minimal set of observers" for the partitioned by clause. The reason is because there are some abstractions that cannot rely only on their observers to detect a difference between two terms if their is one. Stacks are one example. You can't tell if two stacks are the same or not unless you deconstruct them with the constructor operation pop. Pop in this case must be part of the partitioned by clause.
We looked at a few examples of proof by induction on the number of generated by operations in a term. We also looked at proofs that two terms are the same using the partitioned by clause. We noted that the partitioned by operations don't necessarily define a practical minimum set of operations for the trait, just a set that will allow us to determine differences if there are any.
We finshed the class by introducing a step-by-step procedure for constructing traits. This involved first defining the signature (operation syntax), then partitioning the operations into basic constructors, extra constructors, basic observers, and extra observers, then constructing the candidate left-hand sides for the axioms, then modeling the right-hand sides.
We completed our discussion of trait specifications by fleshing out the trait specification design process and looking at internal consistency and the corresponding concept of sufficient completeness. We then began to discuss interface speficiations.
Both of the trait specification issues in this class involved a discussion of modeling, or (human) design. The process of writing the right-hand sides of the axioms is really the process of formalizing our informal model of our trait. We must have some sort of "picture" in our head or on paper that we use to craft the axioms. In fact, we can view this axiom creation process as proving theorems about our informal model. For example, in a stack we might have the axiom pop(push(s,item)) = s. This axiom says that pops cancel pushes out. The way we would verify that this axiom is the one we want would be by drawing a picture of a stack, pushing an item onto the stack, popping it off of the stack, and observing the result. If we have precise pre- and post-condition semantics defined for our pictorial model, then we can think of this axiom as simply proving that composing these two operations leads to the required result.
The discussion of internal consistency led us to a similar modeling concern. We saw that the paper authors constructed an internally inconsistent set of axiom by knowingly mixing two different models together. One informal model (in their head only) was of an int_bag (recognizing duplicate elements in the bag), and the other was of an int_set (not allowing duplicate elements). We also saw that the reason for the trouble was that axioms were written on non-canonical terms, simplifying the designer's task of creating an inconsistency. Internal inconsistency, by the way, happens when a theorem can be proven to be in more than one equivalence class of terms, an undesirable result (if this were so, then the equivalence classes would be one and the same).
We began our discussion of interface specifications by reiterating that trait specifications deal with values and interface specifications deal with objects. We looked generally over the int_array_spec trait specification, and it's corresponding int_array interface specification. We discussed the informal model behind the int_array_spec, using the specification operation names and our knowledge of dynamic arrays to motivate the discussion. We then looked at the interface specification and noted it's pre and post condition format (requires, modifies, and effects clauses). We also noted that the interface specification defines it's semantics based entirely on the underlying trait specification model.
Assignment:
Important:
We continued to focus on interface specifications, looking today at a few important interface specification issues.
Assignment:
Today we wrapped up our discussion of interface specifications by looking at a number of procedure and abstract data type specifications:
Procedure examples:
array_mult: this procedure focused on normal, modifies, and exceptional behavior parts of the effects predicate, and provided another example of specification behavior based on what has just occurred rather than how it has occurred. Remember, this is an important but subtle point about good specification style.
copy, share, and share_and_copy: these procedures focused on highlighting alias vs. copy semantics. If the difference between these two concepts is understood, then it becomes obvious that share_and_copy is unattainable.
find_in_range: this procedure is another good example of non-deterministic specification, and also demonstrates the trade-off between precision and human comprehension. At first it was not clear to us whether this procedure was choosing an element in an index range or in an element range.
Abstract Data Type example: we looked carefully at the intset abstract data type (from chapter 4), noting the following points:
It was based on the mathematical model of a set, and it's semantics were clear from the understanding of that model. For example, the Union and Difference set operators was used to give an unambiguous definition of the insert and delete operations.
Choose is another example of a non-deterministic operation. There is an important issue here regarding implementor choice. The implementor can choose any implementation that satisfies the specification. Since this specification is non-deterministic, the implementor has some freedom, and can choose the simplest implementation. It is probably overkill to pick a random value in the specification range, and is probably easier to choose the most accessible value in the range.
After looking at the mechanics of two-tiered specification, we looked at some of the main issues of the approach through the paper "The Larch Family of Specification Languages" by Guttag, John V., Hornung, James J., and Wing, Jeannette M., IEEE Software, pps. 24-36, September, 1985.
Important aspects of Larch family - composability, emphasis on presentation, suitability for integrated interactive tools, semantic checking, and localized program dependencies: We talked about the specifications as large, complex, structured objects that are composed from other, smaller pieces, and that this structure is independent of the implementation. A University of Maine master's thesis, "Role of Layered Generic Specifications in Software Reuse", by Chandu Bhavsar, presents a layered window management system as a good example of this. We also talked about the tradeoffs between precision and clarity. A specification must serve as a precise, unambiguous medium for communication between user and implementor, but it also must serve as a clear and straightforward medium for that same purpose.
Kinds of Specifications: we discussed the difference between system specifications, local specifications, and organizational specifications. These seemed to align with the various aspects of documentation and architectural views that we saw in the Hester and Kruchten papers. The important questions of who, what, when, where, and why were also discussed.
Assumptions of the Larch Project: we discussed the following assumptions:
Nondeterminism and incompleteness - these two concepts are very different, and shouldn't be confused with each other. Non-determinism is an important tool in a model based interface specification, but trait specifications are always deterministic. The trait specifications can be incomplete, since they can be used selectively in the description of interface specification behavior.
Although we didn't discuss the issues in this paper completely, other important issues are the use of induction to reason about various parts of the system (proving theorems, validating type information, and verifying implementations), and the need for formal design reviews based on formal specifications and implementations.
We begin to look at the process of formal verification, which we will look at over the next four classes from the point of view of program reasoning and understanding.
We first discussed the difference between total correctness and partial correctness:
The total correctness condition:
If the precondition P is true then the program terminates and the postcondition Q is true
The partial correctness condition:
If the precondition P is true and the program terminates, then the postcondition Q is true
We will see that it is often useful to separate concerns into (1) partial correctness and (2) termination, in order vto simply our correctness argument. There are some interesting issues concerning the proof of each of these concerns, which we will discuss in the context of the Bean Counting problem.
The Bean Counting Problem:
You are given a can of n >= 2 beans, some of which are white and some black. The exact mix is unknown. You are also given an addition supply of white and black beans, enough to be considered "unlimited" for the purposes of this problem. You are asked to repeat the following until there is 1 bean left in the can:
Termination: the termination argument is straightforward. Simply note that each application of the above rules decreases the number of beans in the can by one. Using a simple inductive argument, we can show that there will sooner or later be only one bean left in the can.
Partial Correctness: to prove partial correctness we first need to observe that the loop terminates. We then define a condition that will be always maintained while we are moving toward our answer. We call this very important condition our loop invariant. In this case we note through a number of trials that the evenness/oddness of the number of white beans is maintained (verify this!). So what? Why is this an important property? It becomes important to our postcondition exactly when the stopping condition (1 bean left) is true. Specifically, when the initial number of white beans in the can is even and there is one bean left, then that bean must be black (1 is an odd number). Similarly, if the initial number of white beans is odd, then the final bean must be white.
Note that neither argument, termination or the maintenance of the "even/odd" invariant, in itself gives enough information to imply the postcondition. But they can be reasoned about in a completely separate fashion, and together give us our answer.
Today we looked further at the nature of invariants by exploring a number of examples, including the following loops (where i is the current position in the iteration and n is the number of total iterations):
- a loop to apply a filter to an array. An example of this might be to multiply every value in the array by 4. The loop invariant in this case is:
"the first i values of the array have been transformed to their final values"
This is an extremely simple invariant that is so obvious that you might be tempted to say "so what's the point". But it is exactly this obviousness here that makes this particular example trivial to visualize.
- a loop to compute a factorial. We discussed two ways to compute a factorial - from 1 up to n and from n down to 1. In the first case the invariant is i! and in the second case the invariant is n!/(n-i)!. In both cases the formulas simply to n! when i = n, the loop stopping condition. We will see in the next class that this is exactly how we strengthen a loop invariant to give us the loop post-condition. Note that it is easy for us to visualize both computations, but harder to formalize the invariant for the second. If you were debugging the second program it would arguably be harder to determine that the second was indeed correct before the entire loop was complete.
- a loop to search for the presence of a value in an array - we used a binary search as an example of a less than trivial search program. Note that we would be using two values i and j to represent the current position in the iteration, and that these two values are reflected in the invariant:
"the value we are searching for is not in the array before position i and not in the array after position j"
Note that we search for an answer by search through all of the arrays satisfying the above invariant. We reach the solution when we've found the value in the part of the array remaining, or if j<i. A linear search using only i would have the invariant "the value we are searching for is not in the array before position i".
In all of the above cases the invariant characterizes the search space of the loop. It does not capture whether or not the loop is making progress through that search space! The termination argument is required for that.
The celebrity problem: An invariant can also be used to understand an algorithm in order to improve it. We used the celebrity problem as an example. A celebrity is someone who everyone knows and who knows noone. Suppose we have a simple binary operation "x knows y" available to us for an algorithm. How would we use it to process a set of people to find a celebrity if there is one? We certainly could pick a person and check everyone against that person. If we did this for each person then the invariant would be:
"there either is or isn't a celebrity in the first i people in the set".
This would result in an n-squared algorithm. But suppose we recognized that each application of the "x knows y" operation would definately throw out a person from celebrity consideration. Then we could make one pass through the set to give us a single potential celebrity, then another pass to check just that celebrity, a linear algorithm! The invariant of the first pass is weaker than in the first algorithm:
"There is one potential celebrity in the first i people in the set."
but this is enough to get us the answer with not much additional effort.
Assignment:
We started to look carefully at the formal verfication rulles by listing the topics important to the formal verification process:
We will see that the first three steps allow us to reason about the basic constructs of structured programming, and the fourth and fifth steps allow us to build up our verification toolset by including formal rules using our previously built abstractions. The fifth step allows us to connect the results of our implementation reasoning with the two-tiered specification techniques we developed in the previous section of the course.
We begin by reasonig about straight-line code. The rule we introduce here is the substitution rule for assignment. This rule allows us to "push" a post-condition predicate back acoss an assignment statemnent, sustituting the right-hand side of the assignment statment for the particular variable in question. What we are doing is in effect symbolically undoing the effects of the assignment statement. This gives us the predicate as it should look before we execute the statement.
If we continue to apply the substitution rule through a sequence of assignment statenments, we will derive the "weakest precondition" that must be true in order for us to ultimately achieve our post-condition. The actual pre-condition we start with must imply this weakest precondition. We worked through a simple example in class to demonstrate this. The reason we call the pre-condition a weakest precondition is because we often specify a stonger pre-condition than is necessary to achieve the post-condition effect we want. Another way of looking at this is that the actual pre-condition we start with will yield a "strongest post-condition" that might be a good deal stronger than the post-condition we actually specified.
We discussed a number of examples in our own programming experience in which we wrote a program that gave us a great deal more than we actually needed as a result. This notion of weakest-precondition formalizes that notion.
The selection and loop rules discussed in the next two classes are structural in nature. That is, the subsitution rule forms the basis for all computation, with the selection and loop rules providing the direction for substitution rule application. In the case of the selection rule, we simply reason about two separate sequential paths. In the case of the loop rule, we reason about 0 or more repeatitions of a path. The loop rule is where we come to grips with termination.
We continued our exploration of invariants by looking at the division by repeated subtraction algorithm. This is a good example of moving through the search space of an invariant and stopping when we've strengthened the invariant to find our answer. If we start out with x and y such that we want to find the quotient q and remainder r of x/y, then the total correctness formula is:
x >= 0 and y > 0 {division algorithm} x = q*y + r and 0 <= r < y
The invariant we want to maintain through the search space is:
x = q*y + r and 0 <= r
Note that the only difference between the invariant and the postcondition is the extra condition r < y, which is exactly the negation of the loop control predicate! We will see this rule formalized in the next class.
We can view the body of the loop as a transaction that must be executed "atomically", or "all or not at all" in order for the invariant to be maintained. In fact, the two assignment statements:
r = r - y; q = q + 1
show clearly how the invariant is modified in a controlled way so as not to invalidate the condition that we need to maintain.
We are starting to see how loop invariants are an integral part of the program design process, and that their formalization is simply the next step to a deeper understanding of our programs.
Today we looked specifically at the selection and loop invariant rules. We discussed the formulation of each rule as a consequence below the line in the form of a total correctioness formula and a set of predicates above the line that must be satisfied in order for the consequence to be true.
The selection rule: in order for the total correctness formula P{if b then s1 else s2 end}Q to be true, three predicates need to be satisfied -
Note that we can think of the latter two predicates as follows - if we push Q back across s1, then the logical expression b must have been true, and if we push Q back across s2, then b must have been false.
The loop rule: in order for the total correctness formula P{while b do s end}Q to be true, foive predicates need to be satisfied -
Note that we have separated the argument into separate concerns (recall the bean problem partial correctness and termination arguments) and can reason about each concern separately and in any order. For example, we can argue termination (rule 5) first, without concerning ourselves with the partial correctness condition (rules 1 through 4). Considering partial correctness, we can then argue that the invariant and ~b imply the post condition (rule 4) before we've verified that the invariant actually holds throughout the loop (rule 3).
Note that this argument is similar in structure to a proof by induction, and reasoning about program correctness is one of the reasons why we spend so much time with inductive reasoning in our undergraduate discrete mathematics course.
We began by summarizing the role of formal verification techniques in the design and understanding of good software. A nice text to explore these ideas further is The Science of Programming, by David Gries (Springer-Verlag).
After looking carefully at the mechanics of specification and verification, our next step is to explore the connection between the two. We began by defining two important concepts in the design of an abstract data type implementation:
The rep(resentation) invariant: this is a predicate rep(r) defining the legal values of and abstract data type representation.
Example 1: a set implemented with an ordered array with no duplicates. An unordered array with no duplicates is an illegal representation for a set, independent of the set abstraction.
Example 2: a set implemented as an unordered array with duplicates. Any unordered array with duplicates is a legal representation for a set, again independent of the set abstraction.
Example 3: a long integer abstraction implemented as a linked list of nodes each containing a series of digits, and a parent node containing the sum of the digits. Note that the representation invariant acts in many cases as a consistency constraint when redundant data is used to enhance the efficiency of the data structure. In this case the number of nodes is a function of the sum of the digits in the parent node, and the rep invariant reflects this.
The abstraction function: a function A(r) => a, mapping a representation r onto an abstraction a. This is an integral part of the problem we are dealing with today - mapping implementations onto their specifications. In the case of the examples above, the rep in example 1 probably has a 1-1 correspondence with it's set abstraction, but the rep in example 2 doesn't. In the latter case the arrays [4 2 3 1 1 2] and [3 2 4 1] map onto the same set {1 2 3 4}. Note that set equivalence operators have to take this into account. These two implementations should return true when checked for set equivalence. Note also that the complex linked lists of example 3 map onto integers, and must behave like them.
When reasoning about programs in the previous formal verification section, we were concerned with total correctness formulas of the form:
P{S1 S2 ... Sn}Q.
But P and Q were predicates that were defined in terms of the representation of any abstract operations we wanted to reason about. Our problem is compounded by our need to also maintain the correctness of the abstraction. Suppose we have the rep invariant NO_DUPS(r) and the abstraction function SET_OF(r) (taken from our readings). Our augmented total correctness formula is now:
P and NO_DUPS(r) {abstract operation implementation INSERT(a,i)} Q and NO_DUPS(r') and SET_OF(r') = INSERT(SET_OF(r),i).
We went through the process of defining the abstraction function SET_OF(r) precisely, and then discussed the difficulty in proving theorems of the type of the total correctness formula above:
SET_OF(r) =
if LENGTH(r) = 0 then EMPTY
else INSERT (SET_OF (REMOVEH(r)), FETCH(r, HBOUND(r)))
Note that both the abstraction function and the rep invariant (not shown here) are defined in terms of trait specification values, not in terms of interface specification objects. In addition, the abstraction function above is based on an extensional definition of a set as the sum of it's elements. We "unwound" the SET_OF recursive function for a particular array rep, highlighting this fact.
In summary, we need to include at least a natural language description of the rep invariant and abstraction function in every abstract data type implementation, if only to document the sometimes complex and sometimes implicit mapping between implementation and abstract objects.