This is an HTML rendering of a working paper draft that led to a publication. The publication should always be cited in preference to this draft using the following reference:
Diomidis Spinellis, Sophia Drossopoulou, and Susan Eisenbach
Department of Computing
Imperial College of Science, Technology and Medicine
180 Queen's Gate, London SW7 2BZ
We became interested in multi-language object models while researching problems related to multiparadigm programming [SDE94a,SDE94b]. It is widely accepted that different types of tasks can be best implemented in different paradigms. As an example the logic programming paradigm is particularly well suited for implementing expert systems, while many operations on lists can be elegantly described in the functional programming paradigm. Multiparadigm programming can allow each part of a system to be implemented in the most suitable paradigm. Some of the problems in achieving this ideal are:
An object can be used as the abstraction mechanism for code written in a given paradigm. Such objects need to have at least three instance variables (Figure 1):
Every object has at least one method:
As an example, given the imperative paradigm and its concrete realisation in the form of Modula-2 [Wir85] programs, an object written in the imperative paradigm corresponds to a Modula-2 module. The source code variable of that object contains the source code of the module, the object code variable contains the compiled source, and the module state variable contains the contents of the global variables. In addition, the instance initialisation method is the initialisation code found delimited between BEGIN and END in the module body.
All classes contain at least one class variable (Figure 1):
Taking as a paradigm class example, the logic programming paradigm realised as Prolog compiled into Warren abstract machine instructions [War83], the class state variable contains the heap, stack and trail needed by the abstract machine. In addition, the compilation method is the compiler translating Prolog clauses into abstract instructions, the class initialisation method is the code initialising the abstract machine interpreter, while the execution method is the interpreter itself.
Inheritance is used to bridge the semantic gap between code written in a given paradigm and its execution on a concrete architecture. We regard the programming paradigm of the target architecture as the root class. If it is a uniprocessor architecture it has exactly one object instance, otherwise it has as many instances, as the number of processors. The execution method is implemented by the processor hardware and the class_state is contained in the processor registers. The compiled code and module state variables are kept in the processor's instruction and data memory respectively.
From the root class we build a hierarchy of paradigms based on their semantic and syntactic relationships. Each subclass inherits the methods of its parent class, and can thus use them to implement a more sophisticated paradigm. This is achieved because each paradigm class creates a higher level of linguistic abstraction, which its subclasses can use.
As an example most paradigms have a notion of dynamic memory; a class can be created to provide this feature for these paradigms. Two subclasses can be derived from that class, one for programmer-controlled memory allocation and deallocation and another for automatic garbage collection. As another example a simulation paradigm and a communicating sequential processes paradigm can both be subclasses of a coroutine-based paradigm. Subclassing is not only used for the run-time class execution methods. Syntactic (i.e. compile-time) features of paradigms can be captured with it as well. Many constraint logic languages share the syntax of Prolog, thus it is natural to think of a constraint logic paradigm as a subclass of the logic paradigm providing its own solver method, and extension to the Prolog syntax for specifying constraints.
Paradigm inter-operation can be designed around an abstraction we name a call gate. A call gate is an interfacing point between two paradigms, one of which is a direct subclass of the other. We define two types of call gates: the import gate and the export gate. In order for a paradigm to use a service provided by another paradigm (this can be a procedure, clause, function, rule, or a port, depending on the other paradigm) that service must pass thought its import gate. Conversely, on the other paradigm the same service must pass through its export gate. The call gates are design abstractions and not concrete implementation models. They can be implemented manually or automatically by the paradigm compiler, the runtime environment, the end user, or a mixture of the three. Each paradigm provides an import and export gate and documents the conventions used and expected. The input of the export gate and the output of the import gate follow the conventions of the paradigm, while the output of the export gate and the input of the import gate follow the conventions of the paradigms' superclass (Figure 2). The target architecture paradigm combines its import and its export gate using the linked code as the sink for its export gate and the source for its import gate. Call gates can make the paradigm inter-operation transparent to the application programmer and provide global scale inter-operation using only local information.
We must note at this point that the class hierarchy is not visible to the application programmer. The hierarchy is useful for the multiparadigm programming environment implementor, as it provides a structure for building the system, but is irrelevant to the application programmer, who only looks for the most suited paradigm to build his application. This is consistent with the recent trend in object-oriented programming of regarding inheritance as a producer's mechanism [Mey90], that has little to do with the end-user's use of the classes [Coo92].
Using to our approach a multiparadigm programming environment consists of a set of classes, one for each paradigm. The classes are ordered in a hierarchy whose root is the target architecture. Every class is self-contained and only needs to handle the calling conventions of its superclass and provide a mechanism for interfacing with its subclasses. Code in different paradigms is written in different source modules, which are then handled by the appropriate methods of the respective paradigm class.
In order to demonstrate the validity of our approach we have implemented three prototypes:
The integrator is a multiparadigm application dealing with the numeric and symbolic evaluation of integrals. The symbolic evaluation is based on the backtracking resolution mechanism offered by the logic programming paradigm, and the numeric evaluation on the infinite streams implemented in the functional paradigm. Additionally, lexical analysis of the input expressions is described using regular expressions, and the expression grammar is described using a BNF syntax. Finally, expression simplification uses a term rewrite system and graphing of functions is done by directly interacting with Unix tools.
Integrator is implemented in the blueprint multiparadigm programming environment, a prototype implementation of the six paradigms based on our object-oriented approach. Many different implementation techniques have been applied in order to demonstrate the wide applicability of our approach. Some of the paradigms are implemented as compilers (using existing implementations where possible); others are implemented as interpreters.
Finally, the implementation of blueprint is based on a multiparadigm environment generator. This allows the description of paradigms as object classes using paradigm description files. Additionally, it provides a multiparadigm link editor and support for incorporating existing compilers into multiparadigm programming environments.
Some topics that are interesting in the context of the work described above are the following: