CPSC 333 --- Lecture 32 --- Monday, April 1, 1996 Type Definitions, Axiomatic Descriptions, and Schemas (Note: In class I used the term "axiomatic definition" --- and have realized that Wordsworth was calling the same thing "axiomatic description" instead. I'll switch to Wordsworth's term from now on.) These provide ways to package declarations and predicates so that they can be used (and *reused*) more effectively. Definitions of types (as "given sets") should be declared globally. Thus, for the "University Bookstore" example, a specification should begin with the declaration [ Title ] as well as "given set" declarations for all other types (except standard types, particularly ZZ). "Axiomatic Descriptions" can be used to introduce global constants and to "extend" Z by introducing functions (which never change) that you'd like to use throughout your specification. For the "University Bookstore" example, these should be used to introduce the sets ISBN_Number, Price_Paid, etc. --- which should be defined once and never change. Axiomatic Descriptions have two forms. If you wish to declare something *and* include one or more predicates about it, use the form given below (to define "ISBN_Number"): | | ISBN_Number : PP ZZ | |------------------------------------------------------------ | | ISBN_Number = { x : ZZ | (0 \le x) \and (x \le 999999999) } | That is, draw a vertical line near the left margin and a long horizontal line across the page, connected to the vertical line at the left margin. This creates two "regions" --- one above the horizontal line, and one below it. Include the type declaration for the function or constant to be introduced in the region above the horizontal line, and include predicates that define the function or constant more precisely in the region below the horizontal line. If no additional predicates are needed --- that is, the declaration includes all the information you want to introduce, then only one "region" to the right of the vertical line is needed --- so the horizontal line doesn't need to be drawn: | | Price_Paid : PP NN | If declarations and predicates should be collected together and (possibly) reused then they should be included in a "schema". For example, declarations and predicates that specify acceptable states for a "data area" might be included this way. Here is a schema with name "Bookstore_Data" that includes all the declarations and predicates needed to describe acceptable states for the university bookstore's data area --- assuming that all the type declarations and "axiomatic descriptions" needed to introduce types or sets for all attributes have already been given, and that we aren't requiring that only "max_books" can be remembered, etc ----------- Bookstore_Data --------------------------------------- | | books : ISBN_Number \pf (Title x (Price_Paid x (Resale_Price | x (Number_Ordered x (Number_Received x Number_Sold))))) | | courses : (Discipline_Code x Course_Number) \pf | (Course_Title x Number_of_Students) | | publishers : Name \pf (Street x (City x (Province x | (Postal_Code x Telephone_Number)))) | | publications : ISBN_Number \pf Name | | requirements : PP (ISBN_Number x (Discipline_Code | x Course_Number)) | | recommendations : PP (ISBN_Number x (Discipline_Code | x Course_Number)) | |------------------------------------------------------------------ | | dom publications = dom books | | ran publications \subseteq dom publishers | | \forall x : ISBN_Number \bullet | (( \exists y : Discipline_Code x Course_Number \bullet | ((x \mapsto y) \in requirements)) | \implies | (x \in dom books)) | | \forall y : Discipline_Code x Course_Number \bullet | (( \exists x : ISBN_Number \bullet | ((x \mapsto y) \in requirements)) | \implies | (y \in dom courses)) | | \forall x : ISBN_Number \bullet | (( \exists y : Discipline_Code x Course_Number \bullet | ((x \mapsto y) \in recommendations)) | \implies | (x \in dom books)) | | \forall y : Discipline_Code x Course_Number \bullet | (( \exists x : ISBN_Number \bullet | ((x \mapsto y) \in recommendations)) | \implies | (y \in dom courses)) | | \forall x : ISBN_Number \bullet | ((x \in dom books) | \implies | (\exists y : Discipline_Code x Course_Number \bullet | (((x \mapsto y) \in requirements) \or | ((x \mapsto y) \in recommendations)))) | |__________________________________________________________________ Aside from the name, which is written "inside" the horizontal line at the top, a schema always includes a "declaration part" (the region at the top, which includes declarations of variables) and a "predicate part" (the region at the bottom, which includes assertions). As this example should suggest, you draw a schema by drawing a set of lines to form what looks like a large "letter E" which encloses the declaration part and predicate part. Note that I wrote some of the predicates specifying consistency rules for requirements and recommendations a bit differently (and more concisely) than I did in the first lecture on Z. As an exercise, please confirm that the assertions are (essentially) equivalent to the ones in the first lecture. To add the requirement that no more than max_books books can be remembered at once, I'd need to use an axiomatic description to introduce max_books --- *before* the above schema --- | | max_books : NN | |---------------- | | max_books > 0 | ... and then I'd need to include one more assertion in the bottom "predicate part" of the above schema: #books \le max_books The requirements involving max_courses, max_publishers, etc, would be introduced in the same way. The above example serves to show how you can use type declarations, axiomatic descriptions, and schemas to define data areas: Use type declarations and axiomatic descriptions to "globally" introduce types or sets that correspond to attributes. Then use a schema to introduce the system's data area and to define "acceptable" or "correct" states for that data. Next: Using schemas to specify operations. First, I'll switch to a smaller example (to make it possible to finish on time). Let's consider a "counter" that assumes integer values, whose value is bounded below by a negative "min_value" and bounded above by a positive "max_value." First I'll use axiomatic descriptions to introduce the boundary values. | | min_value : ZZ |----------------- | min_value < 0 | | | max_value : ZZ |----------------- | max_value > 0 | Next, I'll use a schema to define the component(s) of, and the acceptable states for, the "Counter": -------- Counter --------------------------------- | | value : ZZ | |-------------------------------------------------- | | (min_value \le value) \and (value \le max_value) | |__________________________________________________ Note that this is slightly different from --- but equivalent to --- the version of this schema that was introduced in class. Schemas for operations use a set of *naming conventions* for variables that are defined below: - Every *input* should be declared in the declaration part and should have a name that ends with ? - Every *output* should be declared in the declaration part and should have a name that ends with ! - "Constants" that were introduced using "axiomatic descriptions" don't need to be declared (again) in the declaration part. - Components of data areas (like "value" for Counter) should actually correspond to *two* declarations in the declaration part of the schema: - Each should be defined once using the original name for the component. This will be used to refer to the component's value *before* the operation begins - Each should also be defined using the original name with an apostrophe or dash --- ' ---- at the end. This refers to the value for the component *after* the operation terminates. Both should be defined to have the same type as was used in the schema where the component was first defined. One more insight that will be helpful later: So far, in mathematics and computer science courses, you've seen two kinds of "variable": - in mathematics a variable x is an *unknown* value of some type. The value of the variable *never changes*. This is the way variables are usually used in proofs - in computer science --- particularly, in computer programs --- variables can be thought of as corresponding to registers. The value of a variable certainly *can* change (and this is the main effect of an "assignment statement" Variables in Z are like *variables in mathematics* --- not like variables in computer programs. Thus, we *do* need to use two names (and two variables) to refer to the two values that a component of a data structure can assume before and after an operation is executed (and, this is also why there's no such thing as an "assignment statement" in Z). Keeping this in mind, we can declare some schemas that act on counters. The first initializes a counter to have value 0. --------- Initialize_Counter ----------------------- | | value : ZZ | value' : ZZ | |---------------------------------------------------- | | (min_value \le value) \and (value \le max_value) | (min_value \le value') \and (value' \le max_value) | value' = 0 |____________________________________________________ The middle assertion isn't strictly necessary, since it's implied by the one below it --- but it doesn't hurt. In the next lecture, we'll see that there's a way to use one schema inside another, and that the version of the schema I've given above corresponds to what you'd get by using this feature. The next schema can be used to set the value of the counter. It isn't *quite* as simple as you might expect, since the counter value shouldn't be changed (at least, it shouldn't be set to the input value) if the input value is smaller than min_value or larger than max_value. Note that the naming convention for inputs has been used... ----------- Set_Counter -------------------------------- | | value : ZZ | value' : ZZ | value_received? : ZZ | |-------------------------------------------------------- | | (min_value \le value) \and (value \le max_value) | (min_value \le value') \and (value' \le max_value) | | (((min_value \le value_received?) \and | (value_received? \le max_value)) | \implies | (value' = value_received?)) | | ((\not ((min_value \le value_received?) \and | (value_received? \le max_value))) | \implies | (value' = value)) | |________________________________________________________ Here's a specification for an operation that simply reports the counter value as an output (and which allows me to include an "output value".) ---------- Report_Counter ------------------------------ | | value : ZZ | value' : ZZ | value_reported! : ZZ | |-------------------------------------------------------- | | (min_value \le value) \and (value \le max_value) | (min_value \le value') \and (value' \le max_value) | | value' = value | value_reported! = value | |________________________________________________________ Note again, that since "=" is a *comparison* operator that tests equality --- and *not* an assignment statement --- I could have replaced the bottom predicate with value = value_reported! and the schema would still have specified the same operation (in the sense that it would have specified the same relationships between the counter's value before the operation, the counter's value after the operation, and the output). Here's a schema for one more operation that would likely be useful. Note again that it isn't *quite* as simple as you might expect, because of the desire to keep the counter value "in range." --------- Increment_Counter --------------------------- | | value : ZZ | value' : ZZ | |------------------------------------------------------- | | (min_value \le value) \and (value \le max_value) | (min_value \le value') \and (value' \le max_value) | | (value < max_value) \implies (value' = value + 1) | (value = max_value) \implies (value' = value) | |_______________________________________________________ Exercise for next time: Write a schema for "Decrement_Counter" that does what you might expect it to, considering its name (and the requirement that the counter value always stay "in range". Then write a schema called "Increase_Counter" which takes a given input, "increment_value?", and then adds "increment_value?" to the counter's value --- or sets it to "max_value" if this is smaller than the result, or sets it to "min_value" if this is less than the result (which is possible if the input is negative!) This schema should also report the new value as an output, with name "new_value!" You'll have noticed that the last few predicates have been somewhat repetitious: The first two declarations and predicates were *always* included. Next time, I'll show how these can be included "automatically" by including one schema (namely, "Counter") inside another.