A Freedom For the Right, Yet a Prevention From the Wrong

-- The Beauty and the Power of the Transframe's Interface --

David L. Shang


Index


Introduction

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.

  • Go to index


    The Beauty of Freedom

    Simplicity

    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[]).

  • Go to index
    Variant Number of Inputs and Outputs

    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.

  • Specifying the number or the types of actual parameters is not required:
    	function sprintf (object_stream: ...);
    	sprintf ( "The Position: x=", r*sin(t), "and y=", r*cos(t), ";\n" ), 
    

  • There is no need to hack the input in a byte-wise fashion:
    	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
    		}
    	}
    

  • The variant number is not limited to the last portion of an interface:
    	function createHierarchicalMember 
    	    (	name: char[];
    		spouse_name: char[];
    		parents: char[]...;
    		children: char[]...
    	    ): HierarchicalNode;
    

  • Go to index
    Multiple Interfaces

    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.

  • Go to index
    Parameterized Interfaces

    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.

  • Go to index
    Free-Style Operator Interface Design

    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.

  • Go to index


    The Power of Prevention

    The Danger

    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: which is not right, and invites dangers.

  • Go to index
    Prevention First

    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: or

    Dangerous combination such as (virus, syringe) and (wolf, sheepfold) will be detected at compile time.

  • Go to index
    Stripping Off the Camouflage

    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++.

  • Go to index
    A Comprehensive Entrance Guard

    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.

  • Example 1
    	function foo (x: integer; y: float; z: (float; float)=(4.5,5.6) );
    	foo (23, 56); 
    

  • Example 2
    	function foo < R; S; T > ( (R;S); (S;T); (T;R) );
    	foo ( (anElephant, aDog), (aDog, aRat), (aRat, anElephant) ); 
    

  • Example 3
    	function foo < AT: array > ( x: AT; y: AT.ElementType );
    	foo ((23,45,56), 34); 
    

  • Example 4
    	function foo
    		< T; S > 
    		( x: T[]...;
    		  y: (T[]; S[])
    		  z: (T; S)
    		);
    	foo (	( "string1", "string2", "string3"),
    		( "string4", (23, 45, 56) ),
    		( 'c', 32 )
    	    );
    

  • Go to index