Algebraic Specifications

Perpetually under construction.

The idea behind algebraic specifications is that an abstract data type (henceforth, ADT) should be characterized only by the behaviors of its members (i.e., which are observable via results yielded when certain operations are applied to them) without saying anything about their representation/implementation. To this end, we describe an ADT by giving

- a
**signature**, which is a list of the names and types of the ADT's operations (an operation's type is given by its domain and range), and - a set of equations (called
**axioms**), which conveys the intended meaning (i.e., semantics) of those operations. Not surprisingly, the ADT's operations are modeled by (mathematical) functions.

To illustrate, we use the ADT **stack** as an example.
A stack is a "container" in which items are held one on top of another
(so to speak) and in which both insertions (**push**) and deletions
(**pop**) of items occur at the same end, called the **top**.
Also, only the item occupying the top of the stack is observable.
This kind of structure has a LIFO ("last-in-first-out") pattern
with respect to the relative order in which items arrive and depart.

Stack <Elem>: --the specification is parameterized by the data type --of the elements to be stored in the stack Signature: isEmpty : Stack<Elem> → bool --answers "Is the stack empty?" topOf : Stack<Elem> → Elem --yields item at top of the stack empty : → Stack<Elem> --yields the empty stack pop : Stack<Elem> → Stack<Elem> --yields stack obtained by removing top item push : Stack<Elem> × Elem → Stack<Elem> --yields stack obtained by placing item at top

Notice that we chose to make the range of `pop` be simply
`Stack`, rather than `Stack × Elem`.
The latter would be the more appropriate way to model
a method (in Java, say) that not only removed the element at the top of the
stack but also returned its value.
However, to keep things simple, we will not do that here.

It is useful to classify operations as being either **constructors** or
**observers**. A constructor is one that yields as a result an object of
the **type of interest** (hereafter, **ToI**),
i.e., the type being defined, which here is `Stack`.
Hence, here the constructors are **empty**, **push**, and
**pop**, as all of them have `Stack` as their ranges.

*Note:* A zero-argument function, such as `empty`,
is called a nullary operation.
Note that a zero-argument function is, in effect, a constant.
That is, a constant, such as 5, can be viewed as a nullary operation:
it takes no argument and always yields the same result.
By treating constants and functions in a uniform way,
things tend to be simpler.
*End of Note.*

An observer is any operation that is not a constructor. In most cases, an observer yields information about the state of an object belonging to the ToI. That is, its domain ought to include at least one instance of the ToI, but its range is usually something other than the ToI. (An exception occurs in the case of an observer that yields an embedded instance of the ToI, such as a sublist within a list or a subtree within a tree.)

In particular, every operation listed among those in the specification for
type `T` should be such that `T` appears either among the
domain types or the range type.
(Any operation not meeting this condition ought to be part of the (separate)
specification for some other type `S` that is used in the
specification of type `T`.)

In the specification above, **isEmpty** and **topOf** are the observers.

At this point, we postpone giving the axioms in order to illustrate how
applications of these operations can be understood as describing stacks
or as observing properties of stacks. Using function composition (and
assuming that **Elem** is, say, *Z* (the integers)), we can
form (syntactically valid) expressions such as

The first one is intended to denote the stack containing, in order from bottom to top, -4 and 6. The second one denotes the stack containing 3, 5, and 2. (The 9 was pushed, but then popped.) Hence, a somewhat simpler expression intended to denote the same stack is

Indeed, it seems natural to expect that, for any particular stack, the
"simplest" expression denoting it should include no applications of
`pop`.
Say you want to describe a stack containing v_{1}, v_{2},
..., v_{n}, from bottom to top.
Then the simplest expression for it would be

The result of the third expression above should be either true or false,
according to whether the expression serving as the argument of `isEmpty`
denotes the empty stack. (Clearly, it does, as the stack obtained from
pushing 5 onto an empty stack, then popping, then pushing 2, and then
popping is, indeed, empty.) So the result ought to be true.

The purpose of the axioms is to describe the intended meanings of the operations. This is done by, vaguely speaking, showing how they relate to one another. Rather than try to explain what that means, it may be more useful to illustrate the idea using examples. Here are the axioms for Stack:

**Axioms:**

(1) isEmpty(empty) = true (2) isEmpty(push(s,x)) = false (3) topOf(push(s,x)) = x (4) pop(push(s,x)) = s

It's to be understood that each axiom is universally quantified by all variables appearing in it. That is, (4) should be understood as an abbreviation for

If you understand what a stack is, each of the axioms should be transparent.
Axiom (1), for example, simply says that the stack described by the expression
`empty` has the property of being empty!
Axiom (2) says that any stack obtained by pushing some element x onto
some stack s is not empty. Obviously!
Axiom (3) says that the element x is on top of the stack obtained by
pushing x onto any stack s. Duh!
Finally, Axiom (4) simply says that, if we pop a stack obtained by pushing
some element x onto some stack s, we get the stack s as the result.

By omitting an axiom of the form

we have left this expression, in some sense, undefined. Another way to
deal with such unwanted applications (corresponding, in this case, to
the fact that we intend that **topOf** never be applied to an empty stack)
is to introduce the notion of an "error" value. We shall not do so here.

Notice, too, that there is no axiom of the form

Is this because we never want to allow **topOf** to be applied to a stack
that can be described by an expression of the form pop(s)? **No!**
Rather, it's because we intend for our axioms to be such that every stack
(in the entire universe of possible stacks) be describable either by the
expression *empty* or by an expression of the form *push(s,x)*.
That is, we intend for empty and push to be the **generators** of our
specification, with pop playing the "lesser" role of an **extension**.
(That is, we classify each constructor as either a generator or an extension.)

Is it, indeed, the case that our axioms are such that every possible stack is describable via an expression that is pop-free? We could prove this rigorously, but for now let's just do an example that should convince you of it. Take the expression

Notice that the highlighted subexpression has the form *pop(push(s,x))*.
(In gory detail, this follows by instantiating s by *push(empty, 5)*
and x by *2*.)
By Axiom (4), this subexpression is equivalent to s
(i.e., *push(empty,5)*).
As we may replace any subexpression by one that is equivalent to it
(this is the so-called Leibniz rule), the above expression
is found to be equivalent to

Examining this expression, we see that it, too, has the form
*pop(push(s,x))*.
(Here the instantiations are s : *push(push(empty, 5), 0)*
and x : *3*.)
Applying axiom (4), we get that it is equivalent to

We have succeeded in removing both occurrences of pop from the original expression! Assuming that the reader did not need these things to be spelled out in such detail, we would have written the above "transformation" as follows:

pop(push(push(pop(push(push(empty, 5), 2)), 0), 3)) = < Axiom 4, with s := push(empty, 5) and x := 2 > pop(push(push(push(empty, 5), 0), 3)) = < Axiom 4, with s := push(push(empty, 5), 0) and x := 3 > push(push(empty, 5), 0)

Now consider any stack-expression E. We prove that E is equivalent to a
stack-expression F that is either pop-free or has *pop(empty)* as a
subexpression. (In the latter case, we could view E as being a
"semantically invalid" stack-expression. Or, if we would prefer there not
to be any such thing, we could add the axiom `pop(empty) = empty`,
in which case we could show that all stack-expressions reduce to one
without any applications of `pop`.)
The proof is by mathematical induction on the number of
applications *k* of `pop` in E.

**Basis:** k=0 (i.e., E has no occurrences of "pop").
Then E is pop-free. As (trivially) E is equivalent to itself,
we take F to be E and we're done.

**Induction Step:** k>0. As an induction hypothesis, assume that every
stack-expression with fewer than k occurrences of "pop" is equivalent to
some stack-expression F that is either pop-free or has *pop(empty)*
as a subexpression. If E contains *pop(empty)* as a subexpression,
take F to be E and we're done. Otherwise, E must contain a subexpression
of the form *pop(push(G,H))*, where G is a stack-expression and H
is an Elem-expression. By axiom (4), E is equivalent to G. But G has
fewer than k applications of "pop", so, by the induction hypothesis, it is
equivalent to some expression F as described above. As E = G and G = F,
we also have E = F (equivalence is transitive). End of proof.

As another example of axiom application, we "evaluate" the expression topOf(E), where E is the stack-expression considered above:

topOf(pop(push(push(pop(push(push(empty, 5), 2)), 0), 3))) = < axiom (4), with s := push(pop(push(push(empty, 5), 2)), 0), x := 3 > topOf(push(pop(push(push(empty, 5), 2)), 0)) = < axiom (3), with s:= pop(push(push(empty, 5), 2)), x := 0 > 0

Let's devise an algebraic specification for **queues**.
A queue is similar to a stack, but differs in that insertions ("enqueue")
and deletions ("dequeue") occur at opposite ends (called rear and front,
respectively). Let's adopt the convention that only the item at the front
is observable.
A queue is analogous to a waiting line (e.g., in a grocery store).
In Great Britain (and probably most of the non-U.S. English-speaking part
of the world), the term "queue" is commonly used, rather than
"(waiting) line".
Arrivals and departures from a queue follow a FIFO ("first-in-first-out")
pattern.

Queue<Elem> // To avoid clutter, we shall here abbreviate // Queue<Elem> to, more simply, Queue Signature: isEmpty : Queue → bool --answers "Is queue empty?" frontOf : Queue → Elem --yields item at front of queue empty : → Queue --yields the empty queue deq : Queue → Queue --yields queue obtained by removing front item enq : Queue × Elem → Queue --yields queue obtained by placing item at rear

For purposes of brevity, we've abbreviated "dequeue" as "deq" and "enqueue" as "enq". Before giving the axioms, we note that the signature for Queue is isomorphic to the one we developed for Stack earlier. That is, they are "structurally identical" in that to get one from the other requires only that we rename (some of) the operations (e.g., "topOf" becomes "frontOf", "pop" becomes "deq") and replace each occurrence of "Stack" by "Queue". This illustrates quite clearly that the signature, taken by itself, falls woefully short of providing a clear description of the intended meaning of the operations.

The axioms for Queue are a little trickier than those for Stack:

**Axioms:**

(1) isEmpty(empty) = true (2) isEmpty(enq(q,x)) = false (3) frontOf(enq(q,x)) = { x if isEmpty(q) { frontOf(q) otherwise (4) deq(enq(q,x)) = { empty if isEmpty(q) { enq(deq(q),x) otherwise

Axiom (3) reflects the fact that inserting an item into a queue has no effect upon which item is at the front of the queue, unless the queue had been empty at the time of the insertion (in which case the inserted item is now at the front!).

Axiom (4) reflects the fact that, for a non-empty queue, doing an insertion followed by a deletion has the same effect as doing them in the opposite order. (The same sequence of operations applied to an empty queue leaves it empty, of course.)

In axioms (3) and (4) we see the use of cases. Sometimes it is more
convenient to express this in a linear way. To do so, we use the **if**
function, whose signature is

where T is any type at all. (Hence, it is really a family of functions.) By definition,

In other words, it's a 3-argument function that yields the value of either its 2nd or its 3rd argument according to whether its 1st argument is true or false, respectively.

Using if, we could rewrite axioms (3) and (4) as follows:

(3) frontOf(enq(q,x)) = if(isEmpty(q), x, frontOf(q)) (4) deq(enq(q,x)) = if(isEmpty(q), empty, enq(deq(q),x))

Here is an example of "evaluating" a queue-expression:

enq(deq(deq(enq(enq(enq(empty,2),0),7))),3) = < axiom (4), with q := enq(enq(empty,2),0) and x := 7, and using fact that isEmpty(q) is false by axiom (2) > enq(deq(enq(deq(enq(enq(empty,2),0)),7)),3) = < axiom (4), with q := enq(empty,2) and x := 0, and using fact that isEmpty(q) is false by axiom (2) > enq(deq(enq(enq(deq(enq(empty,2)),0),7)),3) = < axiom (4), with q := empty and x := 2, and using fact that isEmpty(q) is true by axiom (1) > enq(deq(enq(enq(empty,0),7)),3) = < axiom (4), with q := enq(empty,0) and x := 7, and using fact that isEmpty(q) is false by axiom (2) > enq(enq(deq(enq(empty,0)),7),3) = < axiom (4), with q := empty and x := 0, and using fact that isEmpty(q) is true by axiom (1) > enq(enq(empty,7),3)

Now let's specify the **Bag** ADT, also called the **Multiset**
(although the former term seems to be more popular these days).
A bag is similar to a set, which can be thought of as a collection of
elements taken from some universe of elements.
The distinction between the two is that, with respect to a set,
each element of the universe is simply either a member, or not a member.
With a bag, we have the notion that an element may occur in it any number
of times (i.e., zero or more). For example, the set given by the enumeration

is exactly the same as the one given by

However, viewed as bags, they differ because the first contains two occurrences of both 2 and 4, while the second contains only one of each. (In order to make more clear whether an enumeration represents a set or a bag, here we augment the curly braces with vertical bars when enumerating a bag, as follows: {| 4, 2, 0, 2, 3, 4, 7 |}. )

Note that in neither bags nor sets is there a notion of the elements occurring in any particular order. That is, for example, the enumerations { 2, 4, 7, 3, 0 } and { 0, 2, 3, 4, 7 } denote the same set.

Here is a specification for Bags:

Bag (Elem)

isEmpty : Bag --> bool --answers "Is bag empty?" #Occ : Bag × Elem --> Nat --# of occurrences of an item in bag size : Bag --> Nat --total # of occurrences of all items empty : --> Bag --yields bag with no members insert : Bag × Elem --> Bag --yields bag obtained by inserting an elem delete : Bag × Elem --> Bag --yields bag obtained by deleting an elem

(1) isEmpty(empty) = true (2) isEmpty(insert(b,x)) = false (3) #Occ(empty, x) = 0 (4) #Occ(insert(b,x),y)) = { #Occ(b,y) + 1 if x = y { #Occ(b,y) otherwise (5) size(empty) = 0 (6) size(insert(b,x)) = size(b) + 1 (7) delete(empty,y) = empty (8) delete(insert(b,x),y)) = { b if x = y { insert(delete(b,y),x) otherwise

Here is a specification for Sets:

Set (Elem)

isEmpty : Set --> bool --answers "Is set empty?" isIn : Set × Elem --> bool --is item member of set size : Set --> Nat --# of items in set empty : --> Set --yields set with no members insert : Set × Elem --> Set --yields set obtained by inserting an elem delete : Set × Elem --> Set --yields set obtained by deleting an elem

(1) isEmpty(empty) = true (2) isEmpty(insert(s,x)) = false (3) isIn(empty,x) = false (4) isIn(insert(s,x),y)) = { true if x = y { isIn(s,y) otherwise (5) size(empty) = 0 (6) size(insert(s,x)) = { size(s) + 1 if !isIn(s,x) { size(s) otherwise (7) delete(empty,y) = empty (8) delete(insert(s,x),y)) = { delete(s,y) if x = y { insert(delete(s,y),x) otherwiseWhy is axiom (6) of Set more complicated than the corresponding axiom from Bag? Because inserting an element into a bag necessarily yields a bag containing one more item, whereas the same is not true for a set. Indeed, insert(s,x) denotes the same set as s unless x is not a member of s. For example, the set given by the expression

is the same as the one given by the expression

Both expressions correspond to the set containing 9 as a member, but nothing else. We would usually write this set as { 9 }.

Regarding axiom (8) of Set, we see that in the case x = y, it is more
complicated than the corresponding axiom of Bag. This is due, again,
to the fact that an insertion into a set may yield the same set again.
More precisely, to guarantee that *isIn(delete(s,x),x)* yields false
(as we would expect), we must ensure that *delete(s,x)* evaluates to
a set-expression in which NO insertions of x appear. Hence, it is not
enough to remove the "outermost" insertion of x; all nested insertions of
x must be removed as well. To illustrate, let's do evaluations of two
set-expressions: *delete(E,3)* and *delete(F,3)*, where

F : insert(insert(empty, 3), 8)

As E and F both describe the set that we would normally write as {3, 8},
evaluation of each of *delete(E,3)* and *delete(F,3)*
should yield an expression corresponding to the set that we would
usually write as {8}.

Here goes:

delete(E,3) = < defn of E > delete(insert(insert(insert(empty, 3), 8), 3), 3) = < axiom (8), with s := insert(insert(empty, 3), 8), x := 3, y := 3 > delete(insert(insert(empty, 3), 8), 3) = < axiom (8), with s := insert(empty, 3), x := 8, y := 3 > insert(delete(insert(empty, 3), 3), 8) = < axiom (8), with s := empty, x := 3, y := 3 > insert(delete(empty, 3), 8) = < axiom (7), with y := 3 > insert(empty, 8)

As expected, the resulting expression corresponds to the set we would
usually write as { 8 }. As an exercise, evaluate
*delete(F,3)*. You should get the same expression.

Now suppose that we consider *delete(E,3)* and *delete(F,3)*
as bag-expressions.
Using the bag axioms, *delete(F,3)* should rewrite to
*insert(empty, 8)*, exactly as did the set-expression
*delete(F,3)*. On the other hand, the bag-expression
*delete(E,3)* should be transformed into
*insert(insert(empty, 3), 8)*.

**Initial and Final Algebras**

Suppose that E and F are expressions of the ToI type.
Under what conditions should we consider that E = F (i.e., the objects of
type ToI described by E and F, respectively, are one and the same)?
One point of view, called **initial** semantics, says that E = F
holds only if we can prove (using the axioms, of course) that E = F
(i.e., we can transform E into F or vice versa). The other view,
called **final** semantics, says that E = F holds unless it can be
proved (using the axioms of course) otherwise.
As it is unusual to find axioms asserting that two expressions
are non-equivalent, usually to show that E and F refer to distinct objects
we must show that, for some observer o (and some x), o(E,x) is distinct
from o(F,x) (where x denotes any remaining arguments to o).

For example, take E and F to be the set-expressions insert(insert(empty,3),5) and insert(insert(empty,5),3), respectively. Does E = F hold? As we intend for both of them to correspond to the set that we would usually write as { 3, 5 }, we would say YES. However, note that there is no way to transform E to F (or vice versa) using our set axioms. Hence, the initial algebra induced by our axioms has E and F denoting distinct objects! To fix this, we could add an axiom that says, in effect, that the order in which insertions occur is irrelevant. This is expressed by

By applying this axiom repeatedly, we could transform any set-expression
of the form insert(insert(....(insert(empty,x_{1}), x_{2},
x_{3}), ..., x_{n}) into one
of the same form in which the x_{i}'s appear in any order we like.

Viewed from the final algebra point of view, we don't need to add this axiom in order to get that E = F. Rather, the facts that size(E) = size(F) holds and isIn(E,x) = isIn(F,x) holds for all x (namely, for x = 3 and x = 5 both yield true and for any other value of x they both yield false) is enough for us to assert that E = F.

**stuff to come:**
*sufficient completeness:* every expression of the form f(g(...)),
where f is an observer and g is a generator for ToI, should simplify to an
expression not involving the ToI.

*inconsistency:*
if E = F, where E and F are ToI-expressions, then we should not
have an observer f such that f(E) and f(F) are distinct values.