Perfect Developer basic tutorial 1 This page last modified 2011-10-29 (JAC)

How Perfect differs from typical O-O programming languages

If you are already familiar with object-oriented development using e.g. Java, C++ or Eiffel, this section will alert you to some important differences in using Perfect. However, if you are not very familiar with O-O languages, it's probably better to skip this section on a first reading.

Value semantics

In most O-O languages, there is a difference between primitive types (e.g. int) and class types (e.g String in Java, or any class declared by the developer). The difference lies in the way that assignment and parameter passing are carried out. Objects of primitive types are assigned or passed by value; but objects of class types are assigned or copied by reference (if you are using C++, this only happens if you are using variables or parameters declared as references or pointers; but since you can't have polymorphism or dynamic binding if you use plain types, you often have little choice).

In other words: suppose you declare a class BankAccount, then declare two variables myAccount and yourAccount of type BankAccount, and initialize myAccount to a newly-constructed BankAccount object. If you then execute the assignment yourAccount = myAccount (or yourAccount := myAccount depending on the language you are using), then you end up with two variables referring to the same object. Any changes you subsequently make to yourAccount will be reflected in the value you retrieve from myAccount. This is called reference semantics.

By contrast, if you have two variables myInt and yourInt of type int, and you execute the assignment yourInt = myInt, the two variables remain separate (i.e. subsequent changes to one do not affect the other).

There is no logical reason for this inconsistency; the pragmatic reason is that it makes life easier for compiler writers.

Now, reference semantics can be exactly what you want. If you construct a network of interlinked Person objects, you may well want several other objects to refer to the one Person (e.g. as the spouse of a second Person and the mother of a third Person). However, quite often the use of reference semantics gets in the way. For example, suppose you have two String objects. It would be handy to be able to copy one from the other, then modify one without affecting the other. Most O-O languages don't let you do this (Java's attempt at a solution is to make String objects immutable and to use StringBuffer to hold mutable strings; but you had better not copy StringBuffer objects around unless you are very careful indeed!). Also, if you use the equality operator ("==" in Java) between two String objects, you may well find that two apparently identical strings do not compare equal, because the equality operator only returns true if the two variables refer to the same object (and not if they refer to distinct but identical objects!).

The result of being force-fed with reference semantics is that you need to think very carefully where to use simple assignment and equality operations, and where to use deep copy (or clone) and deep equality methods instead. And it gets worse. If an object to be copied or compared refers to other objects, how deep should you go when copying or comparing? One level? Two levels? As deep as possible?

The use of reference semantics also creates problems for proving that specifications and code are correct. The verifier finds it just as hard to track which variables might refer to the same object as you do.

Perfect implements the logical solution: only use reference semantics where the user specifically asks. So, if you declare a variable or parameter of type BankAccount, then when you pass this as a non-modifiable parameter or assign it to a variable, the system behaves as if a copy of the BankAccount object has been made. If a BankAccount contains other objects, those objects appear to be copied as well. If you really want to create a new reference to the same object instead (e.g. to assign a Person to a variable called spouse), just declare its type to be ref Person instead of plain Person.

So: no more worrying about deep copy or deep equality operations! And the Perfect built-in types (int, char etc.) behave just like other final classes (they even have constructors and member methods such as toString).

Polymorphism on demand

Most O-O languages provide polymorphism by default (Ada 95 is an exception). In other words, if you declare class BankAccount and then derive SavingsAccount from it, then anywhere you declare a variable or parameter of type BankAccount, you can assign or pass it a SavingsAccount instead.

The problem with default polymorphism is that it makes the system very hard to verify. A variable of type BankAccount might at run-time contain a value of a type totally unknown to the verifier (maybe the class SavingsAccount hadn't even been written when the verifier was run!). It is certainly possible to design class hierarchies that can be safely extended without breaking validation of client classes, but it does require additional effort. So Perfect assumes that when you say something is of type BankAccount, you mean exactly that.

Where you do want to allow a value of any descendant class, simply declare the type to be from BankAccount instead of plain BankAccount. Semantically, the type 'from T' means 'the union of all non-deferred types in the set comprising T and all its (direct or indirect) descendant classes'.

The meaning of null

In most O-O languages, null is an allowed value of any variable or parameter of a class type (in C++ this only applies to pointer types).

Sometimes this is very useful (e.g. you can use null to initialize a variable that you can't assign a sensible value to yet, or to indicate the terminal nodes in a tree or linked list). However, there are very many situations where a value is not allowed to be null. You may have frequently written comments like 'owner' must not be null here - or you may have given up writing these comments because so many of them are needed!.

In Perfect, null is a value (in fact, the only value) of class void. Therefore, a variable or parameter can only accept the value null if its type includes void. If you want to declare a variable spouse that can be either a Person or the value null, you should declare its type as Person || void (pronounced "person union void"). When you come to retrieve it and want to treat it as a Person (because you "know" it can't be null at that point), you will need to use the expression (spouse is Person) instead of plain spouse.

Type conversions (lack of)

Many O-O languages convert values between types pretty freely (C++ is the worst offender in this respect). The problem is that the combination of automatic type conversion and method overloading is dangerous: it leads to ambiguous matching between method calls and method declarations. Usually the ambiguity is resolved by complicated rules to determine which match is to be preferred over the others (the C++ Standard devotes 14 pages to the subject of overload resolution!).

Perfect allows only one automatic type conversion: an expression of some type may be widened to a type that encompasses the original type. For example, the value 3 can be widened to the type int || void, or a value of type SavingsAccount can be widened to type from BankAccount.

Additional overloaded operators are provided to make up for the lack of automatic type conversions. For example, the expression 2.5 + 1 is valid because there is a version of the + operator defined with operands of type real and int. But if you declare a variable pi of type real, then assigning pi the value 0 isn't allowed (you can assign it the value 0.0 or real{0} instead).

No global or static variables

Perfect is unusual in that it does not provide global variables, or class-wide ('static') variables. This is because verification is greatly complicated by the presence of such variables.

At first this may appear to be a severe limitation. In practice, it is not a problem, although it does mean that you will often need to add an extra parameter to a method to pass data that you might have preferred to be global. If there are several such pieces of data, it often makes sense to declare a simple class to hold all of them, then you can pass them all in a single parameter.

No destructors

Perfect does not provide destructors. This is not a significant problem, as Java users will know (because destructors in Java are of little use, as object destruction may be postponed indefinitely unless the garbage collector is explicitly invoked). In C++, destructors are frequently needed in order to release storage that is not longer required; but Perfect provides semi-automatic memory management, so you generally don't need to worry about releasing storage.

Next:  Verified Design By Contract

 

Save My Place Glossary Language Reference Manual
Tutorials Overview Main site   
Copyright © 1997-2012 Escher Technologies Limited. All rights reserved. Information is subject to change without notice.