Just another day at the office
In the past few weeks I wrote a program to compute the aliases of variables and expressions in an object-oriented program (based on a new theory [1]).
For one of the data structures, I needed a specific notion of equality, so I did the standard thing in Eiffel: redefine the is_equal function inherited from the top class ANY, to implement the desired variant.
The first time I ran the result, I got a postcondition violation. The violated postcondition clauses was not even any that I wrote: it was an original postcondition of is_equal (other: like Current) in ANY, which my redefinition inherited as per the rules of Design by Contract; it reads
symmetric: Result implies other ~ Current
meaning: equality is symmetric, so if Result is true, i.e. the Current object is equal to other, then other must also be equal to Current. (~ is object equality, which applies the local version is is_equal). What was I doing wrong? The structure is a list, so the code iterates on both the current list and the other list:
from
start ; other.start ; Result := True
until (not Result) or after loop
if other.after then Result := False else
Result := (item ~ other.item)
forth ; other.forth
end
end
Simple enough: at each position check whether the item in the current list is equal to the item in the other list, and if so move forth in both the current list and the other one; stop whenever we find two unequal elements, or we exhaust either list as told by after list. (Since is_equal is a function and not produce any side effect, the actual code saves the cursors before the iteration and restores them afterwards. Thanks to Ian Warrington for asking about this point in a comment to this post. The new across loop variant described in two later postings uses external cursors and manages them automatically, so this business of maintaining the cursor manually goes away.)
The problem is that with this algorithm it is possible to return True if the first list was exhausted but not the second, so that the first list is a subset of the other rather than identical. The correction is immediate: add
Result and other_list.after
after the loop; alternatively, enclose the loop in a conditional so that it is only executed if count = other.count (this solution is better since it saves much computation in cases of lists of different sizes, which cannot be equal).
The lesson (other than that I need to be more careful) is that the error was caught immediately, thanks to a postcondition violation — and one that I did not even have to write. Just another day at the office; and let us shed a tear for the poor folks who still program without this kind of capability in their language and development environment.
Reference
[1] Bertrand Meyer: The Theory and Calculus of Aliasing, draft paper, available here.
Hi Bertrand, just wondering about your definition of is_equal with respect to command / query separation? By advancing through the collections, the value of “item” is changing for the current object as well as “other”. Would you need to restore the original values of “item” so as to not produce any abstract side effects?
Kind regards,
Ian
You are right, I had omitted to mention that point. I updated the relevant part of the article to mention the need to save and restore the cursors. Actually in such cases I now use the new “across” loop variant, which uses an external cursor and maintains this automatically, so that one does not need to worry about such issues any more.