A Potpourri of Paradigms (25-Oct-1994)

A Potpourri of Paradigms

Last Updated: 26 Oct 94
 

In this section, we will make a short tour passing a number of common programming paradigms, finally arriving at object-oriented techniques. Suitable background for this section is the ground-breaking book by B. Stroustrup [1], the original creator of the C++ programming language (see also [2]).

Each of the paradigms embodies a set of architectural rules. The rules constrain the activities of design and programming, but it is these constraints that keep us from going awry in our long-term goals.

The design techniques outlined here are not formal; the human mind is the best ``tool'' to support and distinguish between them. (You could e.g. make contact with ``literate programming'' a real tool to help us to leverage the output of a design idea across an entire development community.)

Our basic assumption will be that other than the change from one high-level language like FORTRAN, C or PASCAL to another, Object-oriented programming [OOP] requires a new mind-set.

The paradigms are hierarchical, i.e. if a particular language supports a paradigm, it will in general support all paradigms leading to it as well. Note that we shall speak of support of a paradigm only if the language not merely enables its use but provides suitable mechanisms to allow for elegant, effortless programming employing the paradigm. In a smallish example below, you will see that one can write FORTRAN programs with data hiding (see sect. 2.3) and object-oriented programs using the C language, but these languages do not really provide the necessary means to do this without making the result look unnatural. Nevertheless, at times the borders between realizations of one or the other paradigm may appear fuzzy.


Procedural Programming - ``Use the best algorithms you can find''


  Programming in procedural, or function-oriented style is useful to bring order in the maze of algorithms available to solve a particular problem numerically.

To physicists this is a common style since the advent of FORTRAN, which comes from FORmula TRANslator. ALGOL and C are later inventions in the same direction. A simple example of good procedural programming style is the implementation of the square root function as sqrt(x) in a FORTRAN program.

The procedural programming paradigm is supported by all modern programming languages by facilities for passing arguments to and returning values from functions. It is quite a minimalistic style and we consider it an elementary condition of programming rather than a method of choice.


Modular Programming - ``Hide the data in modules''

 
To understand the modular programming style, let us identify a module as a set of related procedures together with the data they manipulate.

We recognize the need to encapsulate functions or data subsets. This is not just extreme procedural style, but an emphasis shift from the procedures to partition a program in its task to the organization of data defined and manipulated by it. Thus, modularity is also known as the ``data-hiding principle''.

When the names of variables, constants, functions and types are made local to a module, we can also speak more generally of ``information hiding''. In C++, the class concept does provide support for this concept. In the C language, the possibility to separately compile code subfiles (using e.g. the #include compiler directive can pose as an example of modular programming).

Note: When there is no grouping of procedures with related data, the procedural style suffices and there is no need for modular programming.

Example: A linked list.

Consider the example of a useful, self-referential container type, a singly linked list. It is illustrated in the figure below. This particular way to structure data will occupy us a great deal more later on. For now, observe that to be a useful collection of elements, we must provide operations on it, like prepend(), first(), print(), delete(), release() (written with brackets to indicate their function character). Singling out these operations and implementing them as separate functions is an example of modular programming paradigm use.

A linked list illustrated.

Data Abstraction - ``Design your own types and methods''


 

The modular style leads us to the centralization of data of a type. The next step is to transcend the built-in types, like integer, real etc. and create new user-defined types designed to suit our specific needs. This is called Data Abstraction. By a data structure, we mean an ordered collection of data objects and a set of operations on these objects.

A simple example is the definition of a type Complex of number pairs in the C or C++ programming languages (C and C++ do not have a built-in type for complex numbers, as FORTRAN has, for example), or a type Matrix as a field of numbers.

Another name for user-defined types is ``abstract data types'' [ADT], a collection of data and a set of allowed operations (actions, methods) that are used to define and manipulate the data.

A priori, the variables of an ADT do not have a name known to the compiler. They do not obey the usual rules saying when they go in or out of value (this is called the ``scope'' of a variable) etc. The whole machinery available to the not-so-simple built-in types must be defined by us to allow to manipulation of instances of the new type.

DA is to a limited extent supported in FORTRAN 90 (through the MODULE statement) and in C (through the struct statement). In the C++ programming language, the class keyword is reserved for the definition of ADTs.

Example: the linked list revisited.

In the following C++ example, we are following up to the previous linked list example which we used to illustrate the MP style.
class List {
 private:
    Listelement * listHead;
 public:
   List() {listHead = NULL;}
   ~List() { release(); }
   void prepend(char data);
   void print();
   void release();
   // ...
 };
} What you see here is our definition of a C++ class named List between a pair of curly brackets closed by a semicolon. The body of the definition is partioned in two groups labelled private and public. The first group contains only a data member (a pointer variable, more specifically), while the methods needed to manipulate the list are all in the second group. In addition, there are two definitions with the same name as the class, itself, namely List(), and ~List(). The first is called a (default) constructor and provides us with an empty list whose ``head'' pointer points nowhere (to the NULL address). The second is a destructor calling the release() method in order to ``destroy'' the elements of the list one by one - i.e. free the storage space they are occupying.
Apparently, all the other methods declared in this class still need to be written out.

It is convenient to keep the class definition consisting of method declarations only in its own file, e.g. List.hh (the extension indicates that it contains a ``header'' to the code), and the definitions of the methods in a file List.cc which includes the header file(s) using the #include compiler directive. Sometimes you can also see files with the extension *.icc containing methods which are inline expanded the compiler (C++ has an extra keyword, inline, reserved for such functions). The inline method file is included likewise at the end of the header file.

The meaning of the partitioning becomes obvious only when trying to use or instantiate the definitions: outside of the class body, only the use of methods in the public part is allowed and recognized. Thus, anything in this group is truly private to the user-defined type List. On the contrary, the public methods and variables can be used anywhere -- in other ADT definitions or in client programs. The fragment below is part of a sample client. At this point, it shall only show how the ADT might be used in a program called main():

main() {
  List w;
  w.prepend('A');
  w.prepend('B');
  w.print();
  // ...
The file which contains main() must include the class definition. The output, facilitated by the print() method, might simply result in printing the letters added to the (initially empty) list named w. You can see that a period is used as an operator to access the member functions.

Note: When there is no need for more than one object of a type, the modular programming style suffices and there is no need for data abstraction.


Object-orientation - ``Design classes and make commonality explicit''


  One problem with the data abstraction approach is that once an ADT has been defined, it does not really interact with the rest of the program in an obvious way - to adapt it to new uses (like: new operations on its data members), one must modify its definition.

The solution presented in the C++ language is called inheritance. It is the primary characteristic of any object-oriented programming language. A collection of classes or user-defined types exhibits an inheritance hierarchy as one class derives from another (inherits members) in a hierarchical fashion.

A common example is that of a Shapeclass. OOP is defined by expressing the distinction between the general properties of a any shape: it has color (a class data member), it can be drawn (a class method) - and the properties of a specific shape: a Circle is a Shape that has a radius, is drawn by a circle-drawing function etc. In C++, the class Circle is said to be derived from the class Shape, which is said to be a base class of Circle (in another terminology, Shape is called a superclass and Circle a subclass).

Note: When there is no commonality between types selected to become classes, the data abstraction paradigm suffices and there is no need for OOP.

There is another way to express the notion of object-orientation - message passing: Objects are a priori independent of each other. An object of the same or different class can send a message to any object, and the object receiving the message can only act upon that message. We have met an implicit example of such a message above - here, the request for action was to create an empty list. The use of a method like print() is an obvious example for the request to print something - as specified in the implementation of this function (which we did not present).

Example: Message Passing in FORTRAN 77

In the last example, we demonstrate message passing in FORTRAN though nobody would ever attempt to write a FORTRAN like this. Subroutine anObject( msg, I ) Character msg*(*) Integer I Integer*4 aValue If ( msg .eq. "setValue" ) then aValue = I return ElseIf ( msg .eq. "getValue" ) then I = aValue return Else print("Error") EndIf Return End This program defines one character and two integers and sets up a condition for execution depending on the message passed to the block. This is how it would be used in a client program: Call anObject("setValue", 2) Call anObject("getValue", K) The call <function> statements are passing the message to either set or get a value to anObject There is no way to use aValue outside of anObject.

As you go along learning C++, you will have to decide which of the paradigms you need to employ in your program design. It may turn out that you only want to have a ``better C''. It may be that you want to dive deep down in the glittering world of OOP and discover relations among the parts of your application which justify to consider parametric polymorphism - C++ can become arbitrarily complicated and you are well advised to start small and only apply those parts of the language which you have well understood and know how to use.


References


1
B. Stroustrup, The C++ Programming Language (Addison-Wesley, Reading MA, 1992) 2nd ed.
2
S. Sengupta, C.P. Korobkin, C++ Object Oriented Data Structures (Springer, New York, 1994).

Authors:

  • Marcus Speh <marcus@x4u.desy.de>