12.9 Opaque Types Revealed

There was a brief introduction to the concept of opaque types in section 10.7.3, and the reader should review that comment before continuing.

Here, the goal will be to implement an ADT in an opaque way, that is, with the structural details of the type deliberately hidden away in the implementation module so that clients cannot know what they look like.

Here, this will be achieved by revisiting an earlier example. In section 6.9, in the introduction to library modules, the ADT Point was implemented in a transparent manner, that is, with the implementation details available for all to see in the definition module.

TYPE
   Point = ARRAY [1 .. 2] OF REAL;

Client programs "know" the names of the parts of the type Point. The way that they are written may depend heavily on this fact. It is a better idea in practice to keep such details hidden, so that the client program does not have access to such information, but rather require that such data items can be operated upon only by the procedures that are also contained in the module along with the data type definition. This has been referred to, and there have been several instances of predefined library modules containing such opaque types throughout the text.

Object oriented design concentrates on the need for a type of data. The data type and all the operations on it are implemented in such a way that an item of that type can be modified only through those operations, and not by any client program.

There is somewhat more to object oriented design than this in practice, especially if one is working in a notation built from the ground up on such a philosophy. However, this is a good working start for such thinking.

Here is the revised definition of the ADT Point with some procedures changed from functions so that they can return Point in a variable parameter.

DEFINITION MODULE OpaquePoints;

(* based on an original design by R. Sutcliffe
   modified to opaque form by Gord Tisher
   last modification 1995 05 17 *)

   (* angles are measured in radians counterclockwise from the positive x-axis *)

TYPE
  Point;

PROCEDURE newPoint (x, y : REAL; VAR p : Point);
  (* allocates memory for a new point *)

PROCEDURE killPoint (VAR p : Point);
  (* deallocates memory the point variable -- using the variable after a call to killPoint is wrong. *)

PROCEDURE assign (x, y : REAL; VAR p : Point);
   (* returns the abstract point with coordinates x and y *)

PROCEDURE abscissa (p : Point) : REAL;
   (* returns the first, or x-coordinate of the point *)

PROCEDURE ordinate (p : Point) : REAL;
   (* returns the second, or y-coordinate of the point *)

PROCEDURE abs (p : Point) : REAL;
   (* returns the distance from the point to the origin *)

PROCEDURE arg (p : Point) : REAL;
   (* returns the angle to the positive x-axis subtended by a line segment from the origin to the point measured in the range 0 to 2¼lt;¼gt;  radians *)

PROCEDURE polarToRect (abs, arg : REAL; VAR p : Point);
   (* returns the point with the given absolute value and argument *)

PROCEDURE  reflectX (VAR p : Point);
   (* returns the reflection of the point in the x-axis *)

PROCEDURE  reflectY (VAR p: Point );
   (* returns the reflection of the point in the y-axis *)

PROCEDURE reflect0 (VAR p : Point);
   (* returns the reflection of the point in the origin *)

PROCEDURE reflect45 (VAR p : Point);
   (* returns the reflection of the point in the line y = x *)

PROCEDURE scale (VAR p : Point; scaleFactor : REAL);
   (* returns the point with the same argument as p and its absolute value multiplied by the scale factor *)

PROCEDURE rotate (VAR p : Point; rotAngle : REAL);
   (* returns the point with the same absolute value as p and with its argument increased by rotAngle *)

PROCEDURE translate (VAR p : Point; deltaX, deltaY : REAL);
   (* returns the point obtained by shifting the given point deltaX horizontally and deltaY vertically *)

END OpaquePoints.

It is time to clear up a little mystery regarding opaque types that may be troubling the thinking reader. Such types are named in the definition module, but their structure is not given there. How then can the compiler know how much memory to assign statically to such a variable? After all, the earliest that the details can become available is the time when the implementation module is linked to the client program.

In defining Modula-2, Wirth replied that the only possible course of action for the compiler in this case is to assume that opaque types are always a certain fixed size, never more nor less. He concluded that this in turn meant that the opaque type itself would in practice have to be a pointer, and that the data represented by the abstraction would have to be contained in the structure to which it points. Wirth left open the possibility that someone might find some other way to achieve this that would allow opaque types to be other than pointers, and some implementations took advantage of this to the extent that they allowed opaque types to be any type that occupied the same amount of memory as a pointer type. That is, this limitation was a practical one, but not a theoretical one. Although the restriction of opaque types to pointers is clearly an artifact of current technology, in the sense that the limitation results from the standard view of what must happen at compile-link-run time, the ISO standards committee took a much narrower view of the matter:

In an ISO standard conforming implementation, any opaque type defined in a definition module must be redefined in the corresponding implementation module either as another opaque type or as a pointer type.

Any redefinition in terms of another opaque type merely postpones the inevitable--an opaque type is, in the (ISO) end a pointer type. This can be clearly seen in the implementation of the OpaquePoints module below. The type Point is a pointer to the data structure that contains the coordinates of the point, and all access to the actual data must employ the dereferencing operator. Observe that the student who implemented the design chose to use ALLOCATE directly rather than NEW. Note, however, that ALLOCATE and DEALLOCATE appear only once in the entire module, confining their presence to as small a scope as possible, and ensuring good control over memory use.

IMPLEMENTATION MODULE OpaquePoints;

(* based on an original design by R. Sutcliffe
   modified to opaque form by Gord Tischer
   last modification 1995 05 18 *)

FROM Storage IMPORT
  ALLOCATE, DEALLOCATE;

FROM RealMath IMPORT
  sqrt, arctan, sin, cos, pi;

TYPE
  Point = POINTER TO PointData;
  PointData = ARRAY[1..2] OF REAL;


PROCEDURE newPoint (x, y : REAL; VAR p : Point);
BEGIN
  ALLOCATE (p, SIZE (PointData));
  p^[1] := x;
  p^[2] := y;
END newPoint;

PROCEDURE killPoint (VAR p : Point);
  (* deallocates memory the point variable -- using the variable after a call to killPoint is wrong *)
BEGIN
  DEALLOCATE (p, SIZE (PointData));
END killPoint;

PROCEDURE assign (x, y : REAL; VAR p : Point);

BEGIN
  p^[1] := x;
  p^[2] := y;
END assign;

PROCEDURE abscissa (p : Point) : REAL;

BEGIN
  RETURN p^[1];
END abscissa;

PROCEDURE ordinate (p : Point) : REAL;

BEGIN
  RETURN p^[2];
END ordinate;

PROCEDURE abs (p : Point ) : REAL;

BEGIN
  RETURN sqrt (p^[1] * p^[1] +  p^[2] * p^[2]);
END abs;

PROCEDURE arg (p : Point) : REAL;
(* if both coordinates are zero, the angle is not defined, but this procedure will 
  return zero.  No errors are generated. *)

VAR
  temp : REAL;

BEGIN
  IF p^[1] = 0.0 
    THEN
      IF p^[2] > 0.0  (* case of point on positive y-axis *)
        THEN
          RETURN  pi / 2.0;
        ELSIF  p^[2] < 0.0 THEN
          RETURN 1.5* pi (* case of point on negative y-axis *)
        ELSE
          RETURN 0.0;
        END;
    END;
  temp:=  arctan (p^[2]/p^[1]); (* returns first and fourth quadrants only *)
  IF p^[1] > 0.0
    THEN
      IF p^[2] >= 0.0
        THEN
          RETURN temp;
        ELSE
          RETURN (2.0 * pi) + temp;
        END; (* if *)
    ELSE
      RETURN pi + temp (* adjust for second and third quadrants *)
    END;
END arg;

PROCEDURE polarToRect (abs, arg : REAL; VAR p : Point);

VAR
  temp : PointData;
BEGIN
  temp[1] := abs * (cos (arg));
  temp[2] := abs * (sin (arg));
  p^ := temp;
END polarToRect;

PROCEDURE reflectX (VAR p : Point );

BEGIN
  p^[2] := -p^[2];
END reflectX;

PROCEDURE  reflectY (VAR p : Point );

BEGIN
  p^[1] := -p^[1];
END reflectY;

PROCEDURE reflect0 (VAR p : Point );

BEGIN
  p^[1] := -p^[1];
  p^[2] := -p^[2];
END reflect0;

PROCEDURE reflect45 (VAR p : Point );
VAR
  temp : REAL;
BEGIN
  temp := p^[1];
  p^[1] := p^[2];
  p^[2] := temp;
END reflect45;

PROCEDURE  scale (VAR p : Point; scaleFactor : REAL);

BEGIN
  p^[1] := scaleFactor * p^[1];
  p^[2] := scaleFactor * p^[2];
END scale;

PROCEDURE rotate (VAR p : Point; rotAngle : REAL);
VAR
  r, theta : REAL;
BEGIN
  r := abs (p);
  theta := arg (p) + rotAngle;
  polarToRect (r, theta, p);
END rotate;

PROCEDURE translate (VAR p : Point; deltaX, deltaY : REAL);

BEGIN
  p^[1] := p^[1] + deltaX;
  p^[2] := p^[2] + deltaY;
END translate;

END OpaquePoints.

Observe that if the implementor had later decided to change the type PointData and implement it as a record instead, only this implementation module, and not any client program, would have to be re-compiled. Client programs would only have to be re-linked to take advantage of the new code. They need no re-coding themselves to do this, since their code does not depend on the structural details of the opaque type. Indeed, it cannot, for those details are deliberately concealed from the client programs.


Contents