Correspondence

Behavioral refinement is not restricted to the realization of abstract specifications. We will now look at a definition of behavioral refinement, following  [Liskov93], that may serve as a guideline for programmers to define behavioral subtypes, both abstract and concrete, including subtypes extending the behavioral repertoire of their supertypes. In  [Liskov93] the relation between behavioral types is explained by means of a so-called correspondence mapping, that relates a subtype to its (abstract) supertype.
  
slide: The subtype correspondence mapping

A correspondence mapping is a triple consisting of an abstraction function    (that projects the values of the subtype on the value domain of the supertype), a renaming ρ (that defines the relation between methods defined in both types) and an extension map ξ (that defines the meaning of additional methods). See slide 10-correspondence. Technically, the function ξ must be onto, that is each value of the supertype domain must be representable by one or more values of the subtype domain. Generally, when applying the abstraction function, we loose information (which is irrelevant from the perspective of the supertype), for example the specific ordering of items in a container. To determine whether a type σ is a (behavioral) subtype of a type σ, one has to define a correspondence mapping 〈 α, ρ, ξ〉 and check the issues listed in slide 10-subtyping.
  
  
  
slide: Behavioral subtyping constraints

First, syntactically, we must check that the signature of   
  
   and τ satisfy the (signature) subtyping relation defined in the previous chapter. In other words, for each method m associated with the object type τ (which we call mτ), and corresponding method mσ (which is determined by applying the renaming mσ) we must check the (contravariant) function subtyping rule, that is dom( mτ ) \leqslant dom( m σ ) and dom( mτ ) \leqslant dom( m σ ), where ran is the range or result type of m. Secondly, we must check that the behavioral properties of σ respect those of σ. In other words, for each method m occurring in σ we must check that σ and that σ. Moreover, the invariant characterizing σ must respect the invariant characterizing τ, that is τ. The substitutions [ x τ : = α( x σ )] occurring in the behavioral rules are meant to indicate that each variable of type [ x τ : = α( x σ )] must be replaced by a corresponding variable of type [ x τ : = α( x σ )] to which the abstraction function is applied (in order to obtain a value in the (abstract) domain of [ x τ : = α( x σ )]). And thirdly, in the final place, it must be shown that the extension map [ x τ : = α( x σ )] is well-defined. The extension map must be defined in such a way that each method call for an object x of type [ x τ : = α( x σ )], which we write as x.m(a) where a represents the arguments to the call, is mapped to a program x.m(a) in which only calls appear to methods shared by σ and τ (modulo renaming) or external function or method calls. In addition the diamond rule must be satisfied, which means that the states τ and ψ resulting from applying respectively x.m(a) and x.m(a) in state φ must deliver identical values for x from the perspective of φ, that is after applying the abstraction function. In other words, extension maps allow us to understand the effect of adding new methods and to establish whether they endanger behavioral compatibility. In  [Liskov93] a distinction is made between constructors (by which objects are created), mutators (that modify the state or value of an object) and observers (that leave the state of an object unaffected). Extension maps are only needed for mutator methods. Clearly, for observer methods the result of ξ is empty, and constructors are taken care of by the abstraction function.

Behavioral subtypes

The behavioral subtyping rules defined above are applicable to arbitrary (sub)types, and not only to (sub)types defined by inheritance. As an example, we will sketch (still following  [Liskov93]) that a stack may be defined as a behavioral subtype of the type bag. Recall that a bag is a set allowing duplicates. See slide 10-ex-subtype.
  
  • bag - put, get

  • stack - push, pop, settop

  
  • bag - 〈 elems, bound 〉

  • stack - 〈 items, limit 〉

  
put(i):
  require  size( b.elems ) < b.bound
  promise  b′ = 〈 b. elems \uplus {i}, b.bound 〉

push(i):
  require  length( s.items ) < s.limit
  promise  s′ = 〈 s.items ·i, s.limit 〉

slide: Behavioral subtypes -- example

Let the type bag support the methods   
  • bag - put, get

  • stack - push, pop, settop

  
  • bag - 〈 elems, bound 〉

  • stack - 〈 items, limit 〉

  
put(i):
  require  size( b.elems ) < b.bound
  promise  b′ = 〈 b. elems \uplus {i}, b.bound 〉

push(i):
  require  length( s.items ) < s.limit
  promise  s′ = 〈 s.items ·i, s.limit 〉
and get():Int and assume that the type stack supports the methods push(i:Int), push(i:Int) and in addition a method settop(i:Int) that replaces the top element of the stack with its argument. Now, assume that a bag is represented by a pair settop(i:Int), where elems is a multiset (which is a set which may contain multiple elements of the same value) and bound is an integer indicating the maximal number of elements that may be in the bag. Further, we assume that a stack is represented as a pair 〈 items, limit 〉 , where items is a sequence and limit is a the maximal length of the sequence. For example 〈 items, limit 〉 is a legal value of bag and 〈 items, limit 〉 is a legal value of stack. The behavioral constraints for respectively the method put for bag and push for stack are given as pre- and post-conditions in slide 10-ex-subtype. To apply put, we require that the size of the multiset is strictly smaller than the bound and we ensure that the element i is inserted when that pre-condition is satisfied. The multi-set union operator 〈 items, limit 〉 is employed to add the new element to the bag. Similarly, for push we require the length of the sequence to be smaller than the limit of the stack and we then ensure that the element is appended to the sequence. As before, we use the primed variables 〈 items, limit 〉 and 〈 items, limit 〉 to denote the value of respectively the bag b and the stack s after applying the operations, respectively put and push. Proceeding from the characterization of bag and stack we may define the correspondence mapping 〈 α, ρ, ξ〉 as in slide 10-ex-correspondence.
〈 α, ρ, ξ〉
slide: Behavioral subtypes -- correspondence

To map the representation of a stack to the bag representation we use the function mk_set (which is inductively defined to map the empty sequence to the empty set and to transform a non-empty sequence into the union of the one-element multiset of its first element and the result of applying mk_set to the remaining part). The stack limit is left unchanged, since it directly corresponds with the bound of the bag. The renaming function mk_set maps push to put and pop to get, straightforwardly. And, the extension map describes the result of mk_set as the application of (subsequently) mk_set and mk_set.
mk_set
slide: Behavioral subtypes -- proof obligations

With respect to the behavioral definitions given for push and put we have to verify that - pre( put(i) )[ b : = α(s) ] ⇒ pre( push(i) ) and that - pre( put(i) )[ b : = α(s) ] ⇒ pre( push(i) ). These conditions, written out fully in slide 10-ex-proof, are easy to verify. Generally, a formal proof is not really necessary to check that two types satisfy the behavioral subtype relation. As argued in  [Liskov93], the definition of the appropriate behavioral constraints and the formulation of a correspondence mapping is already a significant step towards verifying that the types have the desired behavioral properties.