You are on page 1of 17

Introduction to Object-Oriented Concepts in Fortran95

Object-Oriented Programming (OOP) is a design


philosophy for writing complex software. Its main tool is
the abstract data type, which allows one to program in
terms of higher level concepts than just numbers and
arrays of numbers. In their mathematics, physicists are
quite familiar with the power of abstraction, e.g., we
express physics equations using the curl operator, rather
than writing out all the components. But we have not
used such abstractions very much in our programming.
OOP includes a number of concepts which have
proved useful in programming large projects. These are:
1. Information Hiding and Data Encapsulation
2. Function Overloading or Static Polymorphism
3. Abstract Datatypes, Classes and Objects
4. Inheritance
5. Dynamic Dispatch or Run-Time Polymorphism

Information Hiding and Data Encapsulation


Perhaps the most important concept is that of
information hiding. This means that information which is
required in only one procedure should not be made
known to other procedures which do not need this
information. Like the CIA, procedures should be informed
of data only on a need to know basis. This philosophy
simplifies programming, because there is less detail one
must be concerned about in programming and less
opportunities to make mistakes.
One way to achieve this is to encapsulate the data
inside a derived type, and then allow only certain
procedures (sometime called methods) to modify the data.
One is prevented from modifying the data by any other
means not provided by the programmer.
Such encapsulation permits separation of concerns.
One can separately write and debug pieces of a large
program, without worrying about a new procedure
causing inadvertent damage to an older procedure.
Writing complex program becomes an order N problem,
rather than an order N2 problem.

Lets look at an example of what this means. Consider the


following interface to a legacy Fortran77 fft procedure:
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)
... rest of procedure goes here
In this procedure, f is the data to be transposed, t is a
temporary work array, mixup is a bit reversed table, sct is
a sine/cosine table, indx is the power of 2 defining the
length of the transpose, nx is the size of the f, and nxh is
size of the remaining data, and isign is either the direction
of the transform (-1,1) or a request to initialize the tables
(0).
To use this fft, one must get all of this data correct, there
are many opportunities for mistakes. However, most of
this data is relevant only internal details of performing the
fft. The programmer only wants to worry about the data f
and the direction of the transpose. Life would be much
simpler if one could merely call
call fft1(f,isign)
without having to worry about the other details.

One of the reason all these details are exposed is that


Fortran77 did not allow dynamic arrays. By using
automatic and allocatable arrays, one can easily hide the
scratch array t and the tables mixup and sct inside a
wrapper function:
subroutine fft1(f,indx,isign,nx,nxh)
integer indx, isign, nx, nxh
real f(nx)
complex, dimension(nxh) :: t
integer, dimension(:), allocatable, save :: mixup
complex, dimension(:), allocatable, save :: sct
if (isign==0) allocate(mixup(nxh),sct(nxh))
call fft1r(f,t,isign,mixup,sct,indx,nx,nxh)
Thus the programmer does not have to worry about these
things anymore and there is less opportunity for error.
Fortran95 arrays encapsulate dimension information,
and we can use this feature to remove all the dimension
information from the interface:
subroutine fft1(f,indx,isign)
integer :: indx, isign, nx, nxh
real, dimension(:) :: f
complex, dimension(size(f)/2) :: t
integer, dimension(:), allocatable, save :: mixup
complex, dimension(:), allocatable, save :: sct
nx = size(f); nxh = nx/2
if (isign==0) allocate(mixup(nxh),sct(nxh))
call fft1r(f,t,isign,mixup,sct,indx,nx,nxh)

We have successfully hidden from the programmer


details about the fft that are not necessary to know to use
the fft. Now the interface is much simpler and less error
prone:
call fft1(f,indx,isign)
If one gets the interface down to its bare essentials,
then it is unlikely to change in the future, even if the
internal details of the procedure do change. For example,
suppose on a given computer, there was an optimized fft
which was much faster than the legacy fft1r. One could
now replace the call to fft1r inside the wrapper function,
and the users of the wrapper function would not have to
change anything in their code.
subroutine fft1(f,indx,isign)
...
call faster_fft1r(f,......)
! different internal arguments
end subroutine
call fft1(f,indx,isign)

! Note the call does not change

Thus encapsulation allows one to change the


implementation details of a procedure without impacting
the rest of the program. This also allows concurrent
development: different programmers can be modifying
different pieces of a large program, without worrying
about getting in each others way, so long as the interfaces
do not change.

We can improve this interface even further by noting


that the argument indx which determines the length of the
fft and the internal tables mixup and sct need to be
consistent with one another. The tables are created when
the parameter isign = 0:
call fft1r(f,indx,isign=0)

! create tables

But when the fft is called later, the indx parameter might
different.
call fft1(f,kndx,isign=1)

! wrong value of indx.

Part of the problem here is that the legacy fft1r is actually


used to perform two completely different operations, table
initialization and transposition. It is better to have two
different functions perform two different operations.
But the tables are private arrays stored inside the
wrapper function fft1. How can another procedure
initialize that table? There are several ways to do that.
One way is to put the tables inside a module which is
shared by all the procedures in the module, as follows:

module fft1
integer, save :: saved_indx
integer, dimension(:), allocatable, save :: mixup
complex, dimension(:), allocatable, save :: sct
contains
subroutine new_fft_table(indx)
! create fft tables
integer :: indx, isign, nx, nxh
saved_indx = indx
isign = 0
nx = 2**saved_indx; nxh = nx/2
allocate(mixup(nxh),sct(nxh))
call fft1r(f,t,isign,mixup,sct,indx,nx,nxh)
end subroutine new_fft_table
subroutine fft1(f,isign)
! perform fft
integer :: indx, isign, nx, nxh
real, dimension(:) :: f
complex, dimension(size(f)/2) :: t
nx = 2**saved_indx; nxh = nx/2
call fft1r(f,t,isign,mixup,sct,indx,nx,nxh)
end subroutine fft1
end module fft1
One can use these procedures as follows:
use fft1
call new_fft_table(indx)
call fft1(f,isign=1)

! create new fft table


! indx no longer in argument

We should also create a third procedure in this module


to deallocate the tables if we will no longer perform any
ffts.
subroutine delete_fft_table()
deallocate(mixup,sct)
end subroutine delete_fft_table

! delete fft tables

One other feature we can add is access control. If we add


the following lines to the beginning of the module:
module fft1
private
public: new_fft_table, delete_fft_table, fft1
then the fft tables cannot be accessed from outside the
module. The only way to manipulate the table is via the
new_fft_table and delete_fft_table procedures.
As a student exercise, think about additional error
checks one can add inside these procedures, e.g., how to
prevent calling an fft if the table has not been created.
Thus we have grouped together all operations related
to ffts into a single module. Such grouping is also part of
the concept of encapsulation. Information hiding and data
encapsulation are arguably the most useful and important
concepts in object-oriented programming.

Function Overloading or Static Polymorphism


We have already encountered this concept in earlier
lectures. Function overloading refers to using the same
procedure name but performing different operations
based on argument type. Fortran77 has always had this
feature. For example, the function real() means different
things depending on its type.
integer :: i
real :: a
complex :: z
a = real(i)
a = real(z)

! converts integer to real


! takes real part of complex z

In Fortran95, generic functions allow user defined


functions to also have this feature. For example, there are
many different types of FFTs, real to complex, complex to
complex, one dimensional, two dimensional, single
precision, double precision, etc. In Fortran77 one had
remember different names for each of these FFTs. Since it
is unambiguous what each of these do, Fortran95 allows
one to use the same name for all of them, using generic
interfaces:
interface fft
! define generic name fft
module procedure fft1rc
module procedure fft1cc
...
end interface

so long as each of these functions have different argument


types.
subroutine fft1rc(f)
real, dimension(:) :: f

! argument is real array

subroutine fft1cc(f)
complex, dimension(:) :: f

! argument is complex array

subroutine fft2rc(f)
real, dimension(:,:) :: f

! argument is real 2D array

and so on.
It is easy to overdo function overloading, however.
You should use it only when it is obvious what you intend
to happen. You should avoid using it if it obfuscates your
intention. For example, by overloading different
procedures with a generic name such as solve, you may
not remember later which solver you actually intended,
without carefully studying all the argument types in all
your modules. You still want a human being to be able to
read your code and easily determine what it is supposed
to be doing.
Function overloading is sometimes called static
polymorphism because the actual function being called is
determined (resolved) at compile time and not at run time.

Abstract Datatypes, Classes and Objects


An abstract data type or class encapsulates a user
defined data type along with the operations that one can
perform on that type.
For example, consider a class called Personnel
designed to manipulating personnel records in a database.
(This example comes from Henderson and Zorn). The
data we encapsulate are a persons social security number
and name. The functions we provide create and delete a
record, print a record and obtain a social security number
from a record. In Fortran95 a class looks like:
module Personnel_class
type Personnel
private
integer :: ssn
character*12 :: firstname, lastname
end type Personnel
contains
subroutine new_Personnel(this,s,fn,ln)
...
subroutine delete_Personnel(this)
...
subroutine print_Personnel(this,printssn)
...
function getssn_Personnel(this) result(ssn)
...
end module Personnel_class

A variable of this type is called an object. It is declared


as follows:
type (Personnel) :: person
The components of a derived type are called the class data
members. They are often declared private, so that
individual components are not accessible outside the class.
The procedures defined in the class are called class
member functions. Generally, they provide the only means
by which one can manipulate Personnel objects. One
function which is always necessary is the constructor, to
initialize a record. For example:
subroutine new_Personnel(this,s,fn,ln) ! Constructor
type (Personnel), intent (out) :: this
integer, intent (in) :: s
character(len=*), intent (in) :: fn, ln
this%ssn = s
! store social security number
this%firstname = fn
! store first name
this%lastname = ln
! store last name
end subroutine new_Personnel
We can then create a person record for Paul as follows:
program database
use Personnel
type (Personnel) :: person
call new_Personnel(person,012345678,Paul,Jones)

By convention, the first argument in each method is the


class type, and is commonly called this (in C++) or self (in
other OO languages). In most OO languages, the first
argument is not explicitly declared, but is available.
A destructor is often defined to delete a Personnel
object. In Fortran95, this is only necessary if the Personnel
type has a pointer component. In our case, we will define
a destructor to merely nullify the data:
subroutine delete_Personnel(this)
! Destructor
type (Personnel), intent (inout) :: this
this%ssn = 0
! nullify social security number
this%firstname =
! nullify first name
this%lastname =
! nullify last name
end subroutine delete_Personnel
One can then delete the contents of Pauls person record
as follows:
call delete_Personnel(person)
Since the components of the personnel type are private,
one cannot print them out directly:
print *, person%ssn

! Cannot print out ss number

Instead one has to provide a method to obtain the private


components, for example
print *, getssn_Personnel(person)

! this is OK.

where we define a function to obtain the social security


number:
function getssn_Personnel(this) result(ssn)
type (Personnel), intent (in) :: this
integer :: ssn
ssn = this%ssn
! extract the private component
end function getssn_Personnel
We can also provide a function the print out the entire
record:
subroutine print_Personnel(this)
type (Personnel), intent (in) :: this
print *, this%ssn, this%firstname, this%lastname
end subroutine print_Personnel

Although it may seem a unnecessarily complicated to


make the components private, it has the advantage that
one can change the components and those using this class
do not need to modify their old code. For example,
suppose that we decided at a later date to add an optional
age field to the Personnel type:
type Personnel
private
integer :: ssn, age
character*12 :: firstname, lastname
end type Personnel
We create a new constructor:
subroutine new_Personnel_age(this,s,fn,ln,a)
type (Personnel), intent (out) :: this
integer, intent (in) :: s, a
character(len=*), intent (in) :: fn, ln
this%ssn = s
! store social security number
this%age = a
! store age
this%firstname = fn
! store first name
this%lastname = ln
! store last name
end subroutine new_Personnel_age

We can continue to use the older constructor and the new


one by using generic functions. First we rename the
original constructor:
subroutine new_Personnel_orig(this,s,fn,ln)
Then we define the generic interface
interface new_Personnel
module procedure new_Personnel_orig
module procedure new_Personnel_age
end interface
so that the name new_Personnel now has two meanings.
All of the old code works as before, and new code can use
the new feature:
type (Personnel), dimension(2) :: person
call new_Personnel(person(1),012345678,Paul,Jones)
call new_Personnel(person(2),123456789,Pat,Smith,21)
...
call delete_Personnel(person(1)) ! delete first record
Pat Smiths age is recorded, Pauls is not.
We might also wish to change how the print method
works, e.g., suppose we wish to print to a file instead of
to a console. The print method can be modified
accordingly.

The public interface to a class presents an abstract


type to the outside world. By requiring the outside world
to use only these interfaces, keeping the internal details of
a class private, the internal data cannot be corrupted, and
the implementation of methods can be changed without
impacting others.

You might also like