David L. Shang
The proper design of an interface is one of the most important things in software development. Many, if not the most of, the confusions encountered in software design are problems in the specification of interfaces; and the most frustrated thing in the software life cycle is the interface change.
Interface design is an art. Though general principles exist, the solution is not simple. Different people have different views. An interface is the outside view of a system. As we appreciate a piece of painting by the appearance of its picture, we appreciate a software system by its interface. To a software architect, nothing is more discouraged than being deprived of the freedom to express interfaces in his or her preference.
Yet interface design is also a science. Though a freedom of expression is appreciative, the solution must be precise and reliable. An interface is the channel that connects the outside world to a system. As we need a security door to shut intruders out, we need a precise interface to tell users exactly what they can do and what they cannot. Nothing is more dangerous than a system crash due to an unexpected input that forced in from an unreliable interface.
A freedom for the right, yet a prevention from the wrong -- this is what an interface is supposed to do.
The beauty of freedom is based on a principle of simplicity. A Transframe interface uses two list types, one is for the input, and the other is for the output, to describe what's in and what's out.
Here is an example:
function foo (integer, char[]): (char[], char[]);
Function foo has an interface with an input of the type (integer,char[]) and an output of the type (char[],char[]).
A Transframe interface can take variant number of inputs and generate variant number of outputs in terms of streams and arrays (Transframe's stream is similar to Java's array in flexibility but type-safer, and Transframe's array is similar to C++'s array in efficiency but more powerful.)
Consider an example:
function print (name_list: char[]...);
The notation "T..." is a stream type constructor specifying a type of stream in which an element can be of the subtype of T. The interface of print can take a variant number of character strings:
print ("my_picture.jpg", "my_paper.html", "my_table.ps");
Compared to C/C++'s variant number of inputs, Transframe's support is type-safe, more flexible, and does not require lower level hacking.
function sprintf (object_stream: ...); sprintf ( "The Position: x=", r*sin(t), "and y=", r*cos(t), ";\n" ),
function sprintf (object_stream: ...)
{
foreach (element in object_stream)
inspect (obj=element)
{
when integer: sprintf_int(obj);
when float: sprintf_float(obj);
when char[]: sprintf_string(obj);
// more cases hereafter
}
}
function foo (object_stream: ...)
{ // do something else here
sprintf(object_stream);
}
The jump in C/C++ code is left as an exercise for the reader. It needs a tremendous effort (I don't like nightmares and I apologize for this!)
function createHierarchicalMember ( name: char[]; spouse_name: char[]; parents: char[]...; children: char[]... ): HierarchicalNode;
husband = createHierarchicalMember ( "John Smith", // the member's name "Cindy Smith", // spouse ( "Edward Smith", // father "Alice Smith", // mother "Tom Evans", // father in-law "Mary Evans" // mother in-law ), ( "Jordan Smith", // son "Lilian Smith", // daughter "Dian Smith", // another daughter ) );
The output type of an interface can be a stream. What follows are two member functions defined in the HierarchicalNode class:
function getParentsNames(): char[]...; function getChildrenNames(): char[]...;
They can be used in places where streams are required. For example:
wife = createHierarchicalMember ( "Cindy Smith", husband.name(), husband.getParentsNames(), husband.getChildrenNames() );
It is left as an exercise for the reader to think about an equivalent interface in C++. Multiple level pointer arithmetic (such as char**, or even char***) must be used, and it is extremely clumsy for the actual input parameter presentation, which is quite often beyond the understanding capability of an ordinary programmer.
A function (or precisely, a class) can have multiple interfaces. Each interface is associated with a function body (or precisely, a constructor). Given a function call (or precisely, object instantiation), the interface that has the closest match to the type of the actual input parameters will be selected, and the associated body (constructor) will be executed.
For example,
class sprintf is function (...),(integer),(float),(char[])
{
enter (object_stream: ...)
{
foreach (element in object_stream)
inspect (obj=element)
{
when integer: sprintf(obj);
when float: sprintf(obj);
when char[]: sprintf(obj);
}
};
enter (obj: integer) { /* print integer */ };
enter (obj: float) { /* print float */ };
enter (obj: char[]) { /* print string */ };
}
the function sprintf has four different interfaces. Note that Transframe functions are classes.
It is not new if an interface can be parameterized. But it is new that a paramterized interface supports dynamic binding. Let us consider an example:
function put
< ContentType: any; ContainerType: Container >
( content: ContentType; container: ContainerType );
The function interface is parameterized by two type parameters: ContentType and ContainerType. You might thought that it was equivalent to a C++ template function or an Ada's generic function. You are right if you explicitly declare this function an inline function. But by default, a Transframe function with parameterized interface is called directly without inline expansion. For examples:
put ( 24, anIntegerCollection );
put ( book, aBookshelf );
put ( hydrofluoricacid, anAcidContainer );
The content type and the container type are bound dynamically to the type parameters and the function put is called directly. Therefore, in the first call, ContentType is bound to integer, ContainerType is bound to IntegerCollection, content is bound to 24, and container is bound to anIntegerCollection.
We are not going to worry about extensive use of parameterized interfaces that could expand the application into a giant memory-eater in other languages.
The more important thing is the real polymorphism to handle a type which is unknown at compile time. For example:
content: any = GetContentFromUserInput();
container: Container = GetContainerFromAFactoryOnNet();
put ( content, container );
The put function will take a correct action, responding to the actual type of the content and container. If C++ template function is used, the static expansion will take the face types of content and container, which may be abstract classes and the expansion may end with an error by calling an unimplemented virtual function.
Unlike C++, the format of Transframe's operators are not limited to a small set of built-in ones such as infix, prefix, and postfix. They are generally defined as is in the interface specification. For example,
class array #( ElementType: type; size: integer )
{
function operator [ (x: integer) ] (): ElementType;
};
the interface of the operator [] specifies a format of calling. It should be:
[ anInteger ]It returns an object of the element type of the array.
For a two dimension array, we can define the operator in a different way:
class matrix #( ElementType: type; rows, columns: integer )
{
function operator [ (i,j: integer) ] (): ElementType;
};
the interface of the operator [] specifies a format of calling:
[ anInteger, anotherInteger ]
The format of calling looks exactly as it is declared in the interface. A general operator interface is specified in a format as below:
Compare to C++, Transframe does not have any format limitation to most built-in operators. In addition, Transframe allows user-defined operators. For example,
class Bird is AnimatingObject
{
thread operator
flies ()
to (pos: Position)
fading (fs: FadingStyle);
};
Then we can make expressions that resemble those in a natural language:
swan : Bird(); swan flies to anPosition fading IMAGE_SIZE and COLOR_OPACITY
Transframe's operator interface definition provides not only a flexibility but also a simplicity in the operator expression construction. Transframe expression is divided into 14 priority levels. Expressions in each level (except for primitive expressions) are handled by a common algorithm. The syntactic description of a general Transframe expression requires only a few rules. In contrast, C++ uses several pages to describe the rules and yet results in a less powerful expression. I will discuss Transframe's expression in my future column.
Consider the following function and calls:
function put( content: Content; container: Container); put (virus, syringe); put (wolf, sheepfold);Oh, let's pray that the function put is smart enough to detect all the dangers!
No matter how smart the function put is, the detection is, sometimes, still too late: once the danger is identified, the only method is to crash the whole system instantly with a run-time type exception reported, as it is done in Java when an array element (content) type error is detected in a array (container). Just imagine that a criminal tries to put a flame into a gas tank!
The problem is that the interface fails to establish a security door to prevent any illegal break-through. The interface:
put (content: Content; container: Container)
can be interpreted as below:
Prevention is always better than treatment, because not all bad consequences (run-time exceptions) are curable (recoverable). Therefore, a system interface should be a reliable doorkeeper to stop trouble-makers in precaution.
Many interfaces require that the type of some input should be dependent on the type of some other input. Here are some examples:
put (x: Content; y: Container that accept x's type);
feed (x: animal; f: the food type of "x");
matrixMul (m1: matrix; m2: a matrix with rows = m1's columns);
arrayExtend (a1: array; a2: an array with element type = a1's element type);
The interface must specify the type dependency, which is usually ignored by other languages.
Using Transframe, we can write the put interface as below:
< ContentType >
( content: ContentType; container: Container of ContentType );
or
< ContainerType: Container >
( container: ContainerType; content: ContainerType.ContentType );
which is interpreted as:
Dangerous combination such as (virus, syringe) and (wolf, sheepfold) will be detected at compile time.
Though a wolf cannot wear sheep's clothing (under a variable of the Sheep type) by the subtype rule, it can furnish itself with a false appearance as not being a wolf under a polymorphic animal name:
anAnimal: Animal; anAnimal:= getAnimalFromCarrier(); // a wolf slipped in!
The wolf's camouflage can fool many interfaces without a suitable specification:
function put ( x: Animal; group: array of Animal );but can not deceive a Transframe's interface with the type dependency clearly specified:
function put < AnimalType: Animal > ( x: AnimalType; group: array of AnimalType );
The following call:
put (anAnimal, aSheepArray);
is a compile-time error. The interface acts as a reliable guard: any masked animal trying to enter are required to strip off its camouflage:
assume ( it=anAnimal is Sheep ) put(it, aSheepArray);
// else, please get out!
Transframe provides two kinds of statements, assume and inspect, to detect the real type of a polymorphic variable. When the exact type is required by an interface, the polymorphic variable must be inspected by one of the two statements.
A successfully compiled Transframe program will be guaranteed free of any run-time type errors such as array element type exception in Java and dynamic type casting error in C++.
A good entrance guard should not only resist illegal intruders, but also assist visitors who have the potential to be legally accepted. Such permissiveness can bring about a great deal of convenience. For example, an interface that declares a float input should let an integer to be converted into a float and then allow it to come in.
Transframe has comprehensive type conversion rules that allow many convenient type conversions. Examples are list-to-array, list-to-stream, and list-to-list conversions. We just examine a few examples here.
function foo (x: integer; y: float; z: (float; float)=(4.5,5.6) ); foo (23, 56);
function foo < R; S; T > ( (R;S); (S;T); (T;R) ); foo ( (anElephant, aDog), (aDog, aRat), (aRat, anElephant) );
((Elephant; Dog); (Dog; Rat); (Rat; Elephant))which is converted to a list of the type:
< R=Elephant; S=Dog; T=Rat > ( (R;S); (S;T); (T;R) )that matches the interface. The conversion involves the usage of Transframe's list type conversion rule.
function foo < AT: array > ( x: AT; y: AT.ElementType ); foo ((23,45,56), 34);
((integer; integer; integer); integer)which is converted to a list of the type:
< AT=integer[3] > (AT; AT.ElementType )that matches the interface. The conversion involves the usage of Transframe's list-to-array conversion rule.
function foo < T; S > ( x: T[]...; y: (T[]; S[]) z: (T; S) ); foo ( ( "string1", "string2", "string3"), ( "string4", (23, 45, 56) ), ( 'c', 32 ) );
( ( char[7]; char[7]; char[7] ); ( char[7]; (integer; integer; integer) ); ( char; integer ) )which is converted to a list of the type
< T=char; S=integer > ( stream#(ElementType=array#(ElementType=T); size=3); (array#(ElementType=T; size=7); array#(ElementType=S; size=3)) (T; S) );to match the function interface. The conversion involves the usage of Transframe's list-to-stream, list-to-array, and list-to-list conversion rules.