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 N 2 problem.
Let s 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 other s 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) 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! create fft tables 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 module procedure fft1rc module procedure fft1cc... end interface! define generic name fft
so long as each of these functions have different argument types. subroutine fft1rc(f) real, dimension(:) :: f subroutine fft1cc(f) complex, dimension(:) :: f subroutine fft2rc(f) real, dimension(:,:) :: f! argument is real array! argument is complex array! 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 person s 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 Paul s 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 Smith s age is recorded, Paul s 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.