Department of Physics and Astronomy
University of California at Los Angeles
Los Angeles, CA 90095-1547
and
Jet Propulsion Laboratory, California Institute of Technology
Pasadena, California 91109
email: decyk@physics.ucla.edu
Jet Propulsion Laboratory, California Institute of Technology
MS 168-522, 4800 Oak Grove Drive
Pasadena, California 91109
email: nortonc@olympic.jpl.nasa.gov
Department of Computer Science
and Scientific Computation Research Center (SCOREC)
110 Eighth Street, Rensselaer Polytechnic Institute, Troy, NY 12180-3590
email: szymansk@cs.rpi.edu
Abstract
Fortran90 is a modern, powerful language with features that support important new programming concepts, including those used in object-oriented programming. This paper briefly summarizes how to express the concepts of data encapsulation, function overloading, classes, objects, inheritance, and dynamic dispatching.
Appeared in ACM Fortran Forum, vol. 16, num. 1, April 1997.
Since Fortran90 is backward compatible with Fortran77, it is possible to incorporate these new ideas into old programs in an incremental fashion, enabling the scientist to continue his or her scientific activities. Some of these ideas are useful for the programs typically written by individual authors now. The usefulness of other ideas only becomes apparent for more ambitious programs written by multiple authors. These are programs that might never have been written in Fortran77 because the complexity involved would have been unmanageable. These new ideas enable more productive program development, encourage software collaboration and allow the scientist to use the same abstract concepts in a program that have been used so successfully in scientific theory. Scientific productivity will then improve. Additionally, there is a migration path to parallel computers, since High Performance Fortran (HPF) is also based on Fortran90.
subroutine fft1r(f,t,isign,mixup,sct,indx,nx,nxh) integer isign, indx, nx, nxh, mixup(nxh) real f(nx) complex sct(nxh), t(nxh) c rest of procedure goes here return end
Here f is the array to be transformed (and the output), t is a temporary work array, mixup is a bit-reverse table, sct is a sine/cosine table, indx is the power of 2 defining the length of the transform, and nx (>=2**indx) is the size of the f array, while nxh (=nx/2) is the size of the remaining arrays. The variable isign determines the direction of the transform.
The goal of data encapsulation is make the FFT call look
like:
call fft1r(f,isign)
where all the auxiliary arrays and constants which are needed only by the FFT are hidden inside the FFT, and the rest of the program does not have to be concerned about them. Such encapsulation and information hiding can be achieved by means of automatic, allocatable, and assumed-shape arrays. Since t is a scratch array needed only during the call, it is best treated as an automatic array. The tables needed by the FFT should remain between calls, so they are best treated as saved, allocatable arrays. The input array f is best treated as an assumed-shape array because such arrays know their own size.
With such encapsulation, the author of the FFT can make changes to the algorithm or the internal data structure and the user of the subroutine would not be impacted. Another benefit is that one can hide old Fortran77 procedures behind a modern Fortran90 interface. For example, the Fortran90 FFT can actually call the original Fortran77 FFT inside. Then, the original FFT can be replaced (perhaps with a more optimized version) without the users of the FFT having to modify anything. With these language features, it is easier to develop software collaboratively, since individual authors responsible for individual procedures can make internal changes without affecting the other collaborators.
One powerful new feature of Fortran90 is the ability to
check whether the number of types of arguments to called
procedures are consistent with their declarations. This means
that if one accidentally called the FFT procedure as follows:
call fft1r(f)
the compiler would complain that the argument isign was missing. Argument checking is automatically provided for functions defined in a new program unit called a module.
This ability to check arguments allows function overloading, which refers to using the same function name or operator symbol but performing different operations based on argument type. In Fortran77, this has always been available for intrinsic operations. For example, the '/' symbol gives different results depending on whether its arguments are real, integer, or complex. Fortran90 extends this ability to user defined functions and operators by means of the generic function mechanism. Note that function overloading is done at compile time and does not incur any performance penalty during execution.
For example, if we have another FFT procedure called fft2r
which works on 2 dimensional data:
subroutine fft2r(f,isign)
real, dimension(:,:), intent(inout) :: f
integer, intent(in) :: isign
then both FFT procedures can be given the same name fft (which is
now overloaded) because the arguments f are of different types: in
one case f is a 1d array, and in the other it is a 2d array. If
the both procedures are in one module, this overloading is done
with the following declaration:
interface fft
module procedure fft1r, fft2r
end interface
type private_complex real :: real, imaginary end type private_complex
To create variables a, b, and c of this new type, one makes the
following declaration:
type (private_complex) :: a, b, c
The components of this new type are accessed with the '%' symbol,
and one can assign values as follows:
a%real = 1.0
a%imaginary = 2.0
If we write a procedure for multiplication of the private_complex
types, it makes sense to place the new derived type together with
the procedures which operate on that type into the same module, as
follows:
module private_complex_module
! define private_complex type
type private_complex
private
real :: real, imaginary
end type private_complex
contains
function pc_mult(a,b) result(c)
! multiply private_complex variables
type (private_complex), intent(in) :: a, b
type (private_complex) :: c
c%real = a%real*b%real - a%imaginary*b%imaginary
c%imaginary = a%real*b%imaginary + a%imaginary*b%real
end function pc_mult
end module private_complex_module
What we have created here is a simple class. It consists of a
single derived type definition (whose components are the class
data members) along with the procedures which operate only on that
type (called the class member functions or methods). The actual
variable of type private_complex is called the object.
Multiplication of two objects is performed by calling the
procedure:
c = pc_mult(a,b)
In this example we have made the components of private_complex type PRIVATE so that they are directly accessible only to the class member functions. In other words, the object a is available in the main program, but the components a%real and a%imaginary are not. Then any changes made to the internal representation of the private_complex type (for example, switching to polar coordinates from Cartesian) would be confined to this module and would not impact program units in other modules (assuming the public subroutine arguments remain the same).
The example of private_complex is perhaps academic, since the complex type already exists in Fortran. However, similar techniques can be used to create more powerful and interesting classes to represent others kinds of algebras and thereby program at the same high level with which one can do mathematics, with all the power and safety such abstractions give.
As an example, suppose we want to extend the private_complex
class so that it keeps track of the last operation performed.
Such a feature could be useful in debugging, for example. Except
for the additional feature of monitoring operations, we would like
this extended class to behave exactly like the private_complex
class. We can accomplish this by creating a new class called the
monitor_complex_class that "uses" the types and procedures defined
in the private_complex_class. In this new class, we define a new
derived type, as follows:
type monitor_complex
type (private_complex) :: pc
character*8 :: last_op
end type monitor_complex
which contains one instance of a private_complex type plus an
additional character component to be used for monitoring. Then we
extend all the procedures defined in the private_complex class so
that they work in the monitor_complex class. For multiplication,
we create a mc_mult procedure which calls pc_mult on the
private_complex part of monitor_complex as follows:
module monitor_complex_class
use private_complex_class
type monitor_complex
private
type (private_complex) :: pc
character*8 :: last_op
end type monitor_complex
contains
function mc_mult(a,b) result(c)
type (monitor_complex), intent(in) :: a, b
type (monitor_complex) :: c
c%pc = pc_mult(a%pc,b%pc)
c%last_op = 'MULTIPLY'
end function mc_mult
end module monitor_complex_class
If we overload the '*' operator to refer to pc_mult and mc_mult
with the INTERFACE statement:
interface operator(*)
module procedure pc_mult, mc_mult
end interface
then the multiplication of each type looks the same
type (private_complex) :: a, b, c
type (monitor_complex) :: x, y, z
c = a*b ! multiplication with private_complex types
z = x*y ! multiplication with monitor_complex types
but multiplication in the derived monitor_complex behaves differently than multiplication in the base class private_complex because it stores the last_op component.
subroutine work(a) type (private_complex), intent(inout) :: a a = a*a call display(a,'work:') end subroutine work
where some appropriate display procedure had been defined. Since object a has been declared of type private_complex, this work subroutine will not function if a monitor_complex type was passed, even though multiplication and display are defined for that type. One could, of course, write another work procedure which is identical to this one except that object a is declared of type monitor_complex. This can be tedious and error prone, however. Dynamic dispatching allows one to write a single procedure that would work with both types. In object-oriented languages, such capabilities are normally available automatically. In Fortran90, one must write a special subtype class to provide this functionality. Details of how this class is constructed are beyond the scope of this article, but the interested reader can refer to our other publications.