Introduction
Overview
Teaching: 5 min
Exercises: 0 minQuestions
What is object oriented design?
What features does Fortran include to support object oriented design?
Objectives
Gain a high level understanding of object oriented design (OOP).
Know some of the features that have been introduced to Fortran to support OOP.
Many modern languages support the practice of Object Oriented Programming (OOP). The basic idea here is that data and functionality can be grouped together into a single unit.
There are some basic principles of OOP:
- Abstraction: wrap up complex actions into simple verbs.
- Encapsulation: keep state and logic internal.
- Inheritance: new types can inherit properties and function from existing types and modify and extend those.
- Polymorphism: different types of objects can have the same methods that are internally handled differently depending on the type.
Many features have been added to Fortran over the years which has allowed Fortran programs to be written with increasingly object-oriented designs if desired.
However, I should note that care should be taken while designing programs to make use of OOP practices. If not carefully designed OOP can result in very bad outcomes for performance. It has been stated that premature optimization is the root of all evil, and to some degree that is true. In that you shouldn’t worry too much about optimizing code until you know what is important to optimize, e.g. what takes most of the time. However, when using OOP designs some consideration needs to be given up front to performance otherwise significant re-writing will have to happen to allow for later optimizations. For example, it is often a bad idea to have an array of objects and one should instead favor an object containing arrays.
Here is an example, as a grad student I was writing a hydrodynamics code for a course. I made every cell in my grid an object with members like, density, pressure and velocities. To create my grid I made an array of cell objects and for every cell I called a constructor to initialize it. More over, when updating a property such as density over the whole grid, all the other cell properties had to be loaded into the smaller and faster caches along with it. This would increase the likely hood of cache misses and cause data to be loaded/unloaded into the caches more than otherwise needed. I got very poor performance as compared to other students who didn’t use an OOP style. That isn’t to say I couldn’t have used an OOP style and gotten comparable performance, if I for example made the grid an object which contained arrays of the basic properties of the fluid. So blindly applying the ideas of OOP can have some serious performance implications and require some major structural changes to code if left to later optimization stages. Some up front thought about performance isn’t always a bad thing. The trick is not to worry about details too early, but rather try to limit it to larger code design decisions.
There are of course benefits to the basic OOP principles, in particular they help to reduce the mental load when dealing with large complex code bases by allowing the person reading the code to get a high level understanding of what is going on by abstracting away details. It also helps by reducing the need for duplicating code by using inheritance and polymorphism to allow the same code to operate on different data types. This reduces the amount of code that needs to be debugged and maintained.
The above principles of OOP should be considered as general guidelines rather than perfect rules to always follow.
- Fortran 90 introduced:
- modules
- derived data types
- interface blocks
- operator overloading
- Fortran 2003 introduced:
- type extension
- type-bound procedures
In this part of the workshop we will briefly introduce these features and their role in object oriented programming in Fortran.
Key Points
Fortran supports object oriented design.
Some consideration for performance should be taken with object oriented design early on, e.g. create objects containing arrays rather than arrays of objects.
Modules
Overview
Teaching: 10 min
Exercises: 10 minQuestions
What are modules?
Why would I want to use a module?
Can I control what can be accessed from outside a module?
Objectives
Create a module.
Modules allow procedures (functions and subroutines) and variables to be grouped together as well as some other constructs we will talk about later.
A module is declared as shown below.
modules <module name>
<variable declarations go here>
contains
<procedures go here>
end module
The contains
keyword separates the variable declarations and the procedure implementations.
Modules are not strictly an object oriented only Fortran feature but are very handy to group together related object oriented constructs as we shall see.
Here is an example of a Fortran module.
module m_common
implicit none
integer:: value_one
real:: value_two
contains
subroutine print_values()
implicit none
print *, "value_one=",value_one
print *, "value_two=",value_two
end subroutine
end module
program main
use m_common
implicit none
value_one=1
value_two=2
call print_values()
end program
In the program main
, to be able to accesses the variables and procedures defined in the module you must indicate you wish to be able to access them with the use
keyword followed by the module name you would like your program or procedure to have access to.
By grouping related procedures and variables together into a module it can help to improve re-usability and allow modules to be used in multiple places in code.
Lets download, compile and run the above modules.f90
program.
$ wget https://raw.githubusercontent.com/acenet-arc/fortran_oop_as_a_second_language/gh-pages/code/modules.f90
$ gfortran modules.f90 -o modules
$ ./modules
value_one= 1
value_two= 2.00000000
Access Modifiers
It is possible to control how variables and procedures declared in a module are accessed from outside the module. This can be done either on module wide basis or for specific procedures and variables. If you specify no access modifiers everything will be accessible from outside the module. Access modifiers allows one to apply the OOP principle of encapsulation by restricting access to procedures and variables and keeping them internal.
It is often common in OOP style to have getter and setter procedures to access internal state. Some care should be taken to not get too carried away with this idea as it can lead to writing lots unnecessary code to access private members when they could have just been made public in the first place. Extra code would then require extra work when changes need to be made. However, there are also situations where it can be helpful to make members private that could be problematic if accessed from outside. Care always needs to be taken when applying OOP principles to ensure that they make sense rather than blindly applying those ideas in all situations.
There are two access modifiers:
private
indicates that the procedure or variable can only be accessed within the modulepublic
indicates it can be accessed from outside the module.
Below is an example of using the private
access modifier module wide.
module m_common
implicit none
private
integer:: value_one
real:: value_two
contains
subroutine print_values()
...
end subroutine
end module
program main
use m_common
implicit none
value_one=1
value_two=2
call print_values()
end program
Add the
private
access modifierCopy the
modules.f90
file tomodules_access_none.f90
and add theprivate
access modifier to them_common
module, at the module wide level as shown above. Compile and run if you can. What is the result?Solution
$ cp modules.f90 modules_access_none.f90 $ nano modules_access_none.f90
Then add the
private
access modifier, as shown above, and try to compile with the below command.$ gfortran modules_access_none.f90 -o modules_access_none
You will get the following errors:
modules_access_none.f90:22:11: 22 | value_one=1 | 1 Error: Symbol ‘value_one’ at (1) has no IMPLICIT type modules_access_none.f90:23:11: 23 | value_two=2 | 1 Error: Symbol ‘value_two’ at (1) has no IMPLICIT type
Indicating that the variables
value_one
andvalue_two
can not be accessed from outside the module.
With the private
access modifier variables declared in the module can’t be accessed from outside the module.
Stop using module variables outside the module
If the lines which reference to
value_one
andvalue_two
are commented out and we try to compile again what happens?Solution
$ nano modules_access_none.f90
... program main use m_common implicit none !value_one=1 !value_two=2 call print_values() end program
$ gfortran modules_access_none.f90 -o modules_access_none
then I get an error during the linking process like so:
/usr/bin/ld: /tmp/cc5TwvXi.o: in function `MAIN__': modules_access_none.f90:(.text+0x118): undefined reference to `print_values_' collect2: error: ld returned 1 exit status
Indicating that the subroutine
print_values
can not be accessed either.
As mentioned, individual variables and procedures can be selectively made either private
or public
.
module m_common
implicit none
private
integer:: value_one
real:: value_two
public:: print_values
contains
subroutine print_values()
...
end subroutine
end module
program main
use m_common
implicit none
!value_one=1
!value_two=2
call print_values()
end program
This version will compile and run and will print out the values of the two private variables of the module, however they won’t have been initialized to anything.
Using access modifiers allows certain data and procedures to be inaccessible from the user of these modules. Restricting access to data and procedures is a common practise in object oriented design.
Key Points
Modules are used to package variables, types, and procedures together.
Access to variables and procedures within the module can be controlled with the private and public access modifiers.
Derived Types
Overview
Teaching: 10 min
Exercises: 5 minQuestions
What is a derived type?
How do you use derived types?
Can access to individual components of a derive type be controlled?
Objectives
Create a derived type.
Access members of a derived type.
While modules allow you to package variables together in such a way that you can directly refer to those variables, derived types allow you to package together variables in such a way as to form a new compound variable. With this new compound variable you can refer to it as a group rather than only by the individual components. This can greatly simplify passing a group of related variables to a procedure as only one variable of a given derived type would need to be passed.
To create a new derived type you use the following format.
type <type name>
<member variable declarations>
end type
You can then declare new variables of this derived type as shown.
type(<type name>):: my_variable
Individual elements or members of a derived type variable, or object, can be accessed using the %
operator.
my_variable%member1
The member variable, member1
, would have been declared as part of the derived type.
A full example of creating a new derived type, t_vector
, which holds an array of real
values is shown below.
module m_vector
implicit none
type t_vector
integer:: num_elements
real,dimension(:),allocatable:: elements
end type
end module
program main
use m_vector
implicit none
type(t_vector) numbers
numbers%num_elements=5
allocate(numbers%elements(numbers%num_elements))
numbers%elements(1)=2
print*, "numbers%num_elements=",numbers%num_elements
print*, "numbers%elements(1)=",numbers%elements(1)
end program
$ wget https://raw.githubusercontent.com/acenet-arc/fortran_oop_as_a_second_language/gh-pages/code/derived_types.f90
$ gfortran ./derived_types.f90 -o derived_types
$ ./derived_types
numbers%num_elements= 5
numbers%elements(1)= 2.00000000
In the rest of this workshop we will build on this t_vector
derived type adding functionality as we go. We will create a set of procedures and operators which will allow us to do common operations one might want to perform on a vector abstracting away the details, such as memory management, allowing us to write code at a higher level.
Creating new objects
Creating new vectors is a pretty common thing that we want to do. Lets add some functions to create vectors to reduce the amount of repeated code. Lets create one to make empty vectors, create_empty_vector
and one to create a vector of a given size allocating the required memory to hold all the elements of the vector, create_sized_vector
.
Lets add those functions now.
$ cp derived_types.f90 derived_types_init.f90
$ nano derived_types_init.f90
module m_vector
implicit none
type t_vector
integer:: num_elements
real,dimension(:),allocatable:: elements
end type
contains
type(t_vector) function create_empty_vector()
implicit none
create_empty_vector%num_elements=0
end function
type(t_vector) function create_sized_vector(vec_size)
implicit none
integer,intent(in):: vec_size
create_sized_vector%num_elements=vec_size
allocate(create_sized_vector%elements(vec_size))
end function
end module
program main
use m_vector
implicit none
type(t_vector) numbers_none,numbers_some
numbers_none=create_empty_vector()
print*, "numbers_none%num_elements=",numbers_none%num_elements
numbers_some=create_sized_vector(4)
numbers_some%elements(1)=2
print*, "numbers_some%num_elements=",numbers_some%num_elements
print*, "numbers_some%elements(1)=",numbers_some%elements(1)
end program
Since the function is declared as a t_vector
type, members of the returned t_vector
object can be accessed using the %
operator from an object with the same name as the function from inside the function. This is similar to how functions normally work, however in this case the function return type is a derived type.
Lets compile and run.
$ gfortran ./derived_types_init.f90 -o derived_types_init
$ ./derived_types_init
numbers_none%num_elements= 0
numbers_some%num_elements= 4
numbers_some%elements(1)= 2.00000000
Now we can use these functions to initialize and allocate memory for our vectors.
Deallocating
It is a good idea to match allocations to deallocations. We will add this functionality later in the Destructors episode once we learn a bit more about derived types.
Access modifiers and derived types
Access modifiers can be applied to derived types in a similar way to modules. Here is an example.
module m_vector implicit none type t_vector integer,private:: num_elements real,dimension(:),allocatable:: elements end type ... end module
In this case the member variable
num_elements
could no longer be accessed from outside the module, while theelements
member variable can be.
What is a derived type?
Is a derived type:
a variable which can have multiple variable members
an object which can have multiple member variables
a datatype which can have multiple member variables
a datatype which can contain only a single value like
integer
,real
,etc.Solution
NO: while a derived type can have multiple members, a derived type defines a new datatype where as a variable or object are the instantiation of that derived type or datatype.
NO: while an object has a derived type, the object is not the derived type its self.
Yes: a derived type is a kind of datatype that can have multiple members.
No: derived types can have multiple members, it is possible that they only have one member but they are not restricted to holding a single value.
Key Points
A derived type allows you to package together a number of basic types that can then be thought of collectively as one new derived type.
Access modifiers can be applied within derived types as well as within modules to restrict access to members of that derived type.
Extended Types
Overview
Teaching: 10 min
Exercises: 5 minQuestions
How do you extend a type?
Why can it be useful to extend a type?
Objectives
Create an extended a type.
It is pretty common to use vectors to represent positions in 3D space. Lets create a new derived type which always has only three components. However, it would be really nice if we could reuse our more general vector type to represent one of these specific 3 component vectors. You can do this by using type extension. Type extension allows you to add new members (or not) to an existing type to create a new derived type.
To create a new extended derived type use the following format.
type,extends(<parent type name>):: <child type name>
<member variable declarations>
end type
Here <parent type name>
is the name of a derived type to be extended, and <child type name>
is the name of the new derived type created by extending the parent derived type.
Our new 3 component vector however, doesn’t need any new member variables. However, by having a distinct derived data type for our 3 component vector it will allow us to use specific procedures that work with it as apposed to the those for the more general vector and at the same time reuse common functionality between the two vector types.
Lets create a new t_vector_3
derived type and a create_size_3_vector
function to create new objects.
$ cp derived_types_init.f90 type_extension.f90
$ nano type_extension.f90
module m_vector
implicit none
type t_vector
...
end type
type,extends(t_vector):: t_vector_3
end type
contains
type(t_vector) function create_empty_vector()
...
end function
type(t_vector) function create_sized_vector(vec_size)
...
end function
type(t_vector_3) function create_size_3_vector()
implicit none
create_size_3_vector%num_elements=3
allocate(create_size_3_vector%elements(3))
end function
end module
program main
use m_vector
implicit none
type(t_vector) numbers_none,numbers_some
type(t_vector_3) location
numbers_none=create_empty_vector()
print*, "numbers_none%num_elements=",numbers_none%num_elements
numbers_some=create_sized_vector(4)
numbers_some%elements(1)=2
print*, "numbers_some%num_elements=",numbers_some%num_elements
print*, "numbers_some%elements(1)=",numbers_some%elements(1)
location=create_size_3_vector()
location%elements(1)=1.0
print*, "location%elements(1)=",location%elements(1)
end program
$ gfortran type_extension.f90 -o type_extension
$ ./type_extension
numbers_none%num_elements= 0
numbers_some%num_elements= 4
numbers_some%elements(1)= 2.00000000
location%elements(1)= 1.00000000
Which type is being extended?
In the following code snippet which type is being extended?
... type, extends(B):: A end type ... type(C) function D() implicit none D%thing=1.0 end function ...
A
B
C
D
Solution
No: close, but
A
is the new derived type which extends the existingB
derived type.Yes: the existing derived type
B
is being extended to create a new derived typeA
.No:
C
is a derived type, but from this code snippet it is impossible to tell if it has been extended to a new derived type somewhere else in the code.No:
D
is actually a function name, not a derived type at all.
Key Points
Type extension allows you to build upon an existing derived type to create a new derived type while adding new functionality or modifying existing functionality.
Allows reuse of common code between derived types.
Interface Blocks
Overview
Teaching: 10 min
Exercises: 5 minQuestions
What is an interface block?
How can one be used to create a constructor for a derived type?
Objectives
How do you create an interface block.
As mentioned previously it is a very common task to create new objects of a derived type and perform some initialization of the members. We have already created some functions to do this. One of the functions, create_empty_vector
takes no arguments and creates a new empty vector, while create_sized_vector
creates a new vector of a specific size passed to the function. Both of these functions do the same thing, create a new t_vector
object and initialize it. One might imagine many different such initialization routines, perhaps ones that take another t_vector
or t_vector_3
objects to use to initialize a new t_vector
object as a copy of the passed vector. All of these creation, or initialization, functions do basically the same thing but in a slightly different way depending on the arguments passed to them. It starts to get a bit tedious to have to remember all the names of these different initialization functions. If the compiler could somehow distinguish these functions automatically based on the number and type of arguments rather than the procedure name so that we could call the same generic procedure name and it would pick the correct procedure implementation based on the arguments we passed it.
It turns out there was a feature added to Fortran 2003, called interface blocks which allow multiple procedures to be mapped to one name. The basic syntax of an interface block is as follows.
interface <new-procedure-name>
procedure:: <existing-procedure-name-1>
procedure:: <existing-procedure-name-2>
...
end interface
This allows one to call the procedure <new-procedure-name>
and it will be mapped onto different procedure implementations <existing-procedure-name1>
, <existing-procedure-name-2>
, etc. based on the type, number, and order of arguments passed to the procedure when calling it. Since the number, type, and order of arguments is the only way for the compiler to know which procedure to call, all procedures listed in the interface block must have different types and or number of arguments.
Lets use interface blocks to group our creation functions for t_vector
and t_vector_3
into one procedure name to initialize each of the derived types. It is common to use the name of the derived type as the name of the creation function which returns a new object of that type. Functions defined in this way are referred to as constructors as they construct new objects of the derived type.
$ cp type_extension.f90 interface_blocks.f90
$ nano interface_blocks.f90
module m_vector
implicit none
type t_vector
...
end type
interface t_vector
procedure:: create_empty_vector
procedure:: create_sized_vector
end interface
type,extends(t_vector):: t_vector_3
end type
interface t_vector_3
procedure:: create_size_3_vector
end interface
contains
type(t_vector) function create_empty_vector()
...
end function
type(t_vector) function create_sized_vector(vec_size)
...
end function
type(t_vector_3) function create_size_3_vector()
...
end function
end module
program main
use m_vector
implicit none
type(t_vector) numbers_none,numbers_some
type(t_vector_3) location
numbers_none=t_vector()
print*, "numbers_none%num_elements=",numbers_none%num_elements
numbers_some=t_vector(4)
numbers_some%elements(1)=2
print*, "numbers_some%num_elements=",numbers_some%num_elements
print*, "numbers_some%elements(1)=",numbers_some%elements(1)
location=t_vector_3()
location%elements(1)=1.0
print*, "location%elements(1)=",location%elements(1)
end program
$ gfortran interface_blocks.f90 interface_blocks
$ ./interface_blocks
numbers_none%num_elements= 0
numbers_some%num_elements= 4
numbers_some%elements(1)= 2.00000000
location%elements(1)= 1.00000000
The way we have used interface blocks above is what is called a generic interface as it maps one generic procedure name we specified to multiple specific procedures. This is very similar to what other object oriented languages call overloading.
There is another way to use interface blocks without specifying a generic procedure name to map the listed procedures to, referred to as explicit interfaces. Explicit interface blocks can be used to define a procedure without actually listing the implementation of it. These are useful when using procedures declared in different compilation units which will be linked into the final program later. This is a bit like a forward declaration or a function prototype in C/C++. If your procedures are declared inside a module, as we have been doing, these explicit interfaces are created for you.
Add a procedure to an interface
What happens if we add the
create_size_3_vector
to thet_vector
generic interface? Make a copy of our last source file and add the lineprocedure:: create_size_3_vector
to thet_vector
interface as shown below.$ cp interface_blocks.f90 interface_test.f90 $ nano interface_test.f90
module m_vector implicit none type t_vector ... end type interface t_vector procedure:: create_empty_vector procedure:: create_sized_vector procedure:: create_size_3_vector end interface ...
$ gfortran -o interface_test interface_test.f90
What happens when you try to compile and run it and why?
Solution
During compilation the following error message is printed out
... Error: Ambiguous interfaces in generic interface 't_vector' for ‘create_empty_vector’ at (1) and ‘create_size_3_vector’ at (2) ...
This is because the
create_empty_vector
and thecreate_size_3_vector
functions both have no arguments. Since the compiler uses the number and type of arguments to decide which function to call there is no way for the compiler to know which function should be called when no arguments are given.
Key Points
An interface block can act as either a block of definitions of your procedures (explicit interface) or as a means of associating different procedures with one common name (generic interface).
Procedures that are part of the same generic interface block must be distinguishable from each other based on the number, order, and type of arguments passed.
Break
Overview
Teaching: min
Exercises: minQuestions
Objectives
Key Points
Type Bound Procedures
Overview
Teaching: 10 min
Exercises: 5 minQuestions
What is a type bound procedure?
Can an extended type be passed to a type bound procedure?
Objectives
How do you create a type bound procedure.
We have been printing out parts of our vectors to see how the changes have been making affect them. It would be really nice to have an easy way to see a vector without having to explicitly print out the components of that vector we wish to seen every time.
We can do this by creating a new subroutine display
. We could do this in the usual way shown below.
subroutine display(vec)
...
end subroutine
program main
...
type(t_vector):: vec
...
call display(vec)
...
end program
However, in object oriented languages it is common to think of procedures as part of the object or type and have the procedure be called from the object. So instead of calling a procedure and explicitly passing in the object like call display(vec)
as above the different syntax using the %
operator call vec%display()
is more in line with the object oriented way of thinking.
The %
style of calling a subroutine works exactly the same as the usual way, except that the first argument in the subroutine is automatically replaced by the object to the left of the %
operator. This type of procedure is called a type bound procedure. In other languages this might be called a member function as it is a member of the type, like the component variables are members.
To create a type bound procedure you must specify that the type contains that procedure. Lets add the display
type bound procedure now.
$ cp interface_blocks.f90 type_bound_procedures.f90
$ nano type_bound_procedures.f90
module m_vector
implicit none
type t_vector
integer:: num_elements
real,dimension(:),allocatable:: elements
contains
procedure:: display
end type
interface t_vector
...
end interface
type,extends(t_vector):: t_vector_3
end type
interface t_vector_3
procedure:: create_size_3_vector
end interface
contains
subroutine display(vec)
implicit none
class(t_vector),intent(in):: vec
integer:: i
print*, "t_vector:"
print*, " num_elements=",vec%num_elements
print*, " elements="
do i=1,vec%num_elements
print*, " ",vec%elements(i)
end do
end subroutine
type(t_vector) function create_empty_vector()
...
end function
type(t_vector) function create_sized_vector(vec_size)
...
end function
type(t_vector_3) function create_size_3_vector()
...
end function
end module
program main
use m_vector
implicit none
type(t_vector) numbers_none,numbers_some
type(t_vector_3) location
numbers_none=t_vector()
call numbers_none%display()
numbers_some=t_vector(4)
numbers_some%elements(1)=2
call numbers_some%display()
location=t_vector_3()
location%elements(1)=1.0
call location%display()
end program
Notice that in the display
subroutine we declare the vec
object as class(t_vector):: vec
, rather than the usual type(t_vector):: vec
. This is to indicate that vec
could be either a t_vector
or any derived type which extends this derived type, this would include our t_vector_3
derived type. Allowing any derived types which extended the original base type to be passed into the subroutine using the same argument. This argument can then be treated the same regardless of what type it is since it was derived from a common base type.
This is a pretty neat trick for a statically typed language like Fortran. We can pass in different derived types to the procedure in the same argument. This means that the display
subroutine will actually work for both our t_vector
and our t_vector_3
derived types as t_vector_3
must contain all the same member variables as t_vector
and possibly more. The ability to operate on different types in a similar way is often referred to as polymorphism in object oriented programming languages.
Lets see how this new display
subroutine works.
$ gfortran type_bound_procedures.f90 -o type_bound_procedures
$ ./type_bound_procedures
t_vector:
num_elements= 0
elements=
t_vector:
num_elements= 4
elements=
2.00000000
0.00000000
0.00000000
0.00000000
t_vector:
num_elements= 3
elements=
1.00000000
0.00000000
0.00000000
As you can see, it worked just fine on both our t_vector
objects numbers_none
and numbers_some
and our t_vector_3
object location
.
However, it is still printing out that the object is a t_vector
even when it is a t_vector_3
. It would be nice if we could have it print out t_vector_3
when it is a t_vector_3
object and print out t_vector
when it is a t_vector
object. It turns out there is a way to do this using the select type
construct. It works very much like the select case
construct except that it works with object types instead of values of a variable.
$ cp type_bound_procedures.f90 type_bound_procedures_select_type.f90
$ nano type_bound_procedures_select_type.f90
module m_vector
implicit none
type t_vector
...
end type
interface t_vector
...
end interface
type,extends(t_vector):: t_vector_3
end type
interface t_vector_3
procedure:: create_size_3_vector
end interface
contains
subroutine display(vec)
implicit none
class(t_vector),intent(in):: vec
integer:: i
select type (vec)
class is (t_vector)
print*, "t_vector:"
print*, " num_elements=",vec%num_elements
class is (t_vector_3)
print*, "t_vector_3:"
end select
print*, " elements="
do i=1,vec%num_elements
print*, " ",vec%elements(i)
end do
end subroutine
type(t_vector) function create_empty_vector()
implicit none
create_empty_vector%num_elements=0
end function
type(t_vector) function create_sized_vector(vec_size)
implicit none
integer,intent(in):: vec_size
create_sized_vector%num_elements=vec_size
allocate(create_sized_vector%elements(vec_size))
end function
type(t_vector_3) function create_size_3_vector()
implicit none
create_size_3_vector%num_elements=3
allocate(create_size_3_vector%elements(3))
end function
end module
program main
use m_vector
implicit none
type(t_vector) numbers_none,numbers_some
type(t_vector_3) location
numbers_none=t_vector()
call numbers_none%display()
numbers_some=t_vector(4)
numbers_some%elements(1)=2
call numbers_some%display()
location=t_vector_3()
location%elements(1)=1.0
call location%display()
end program
$ gfortran type_bound_procedures_select_type.f90 -o type_bound_procedures_select_type
$ ./type_bound_procedures_select_type
t_vector:
num_elements= 0
elements=
t_vector:
num_elements= 4
elements=
2.00000000
0.00000000
0.00000000
0.00000000
t_vector_3:
elements=
1.00000000
0.00000000
0.00000000
As you can see, it is now correctly outputting t_vector_3
when the object is of that type.
What is passed to a type bound procedure?
Given the following program
module m_A implicit none type t_B integer:: foo contains procedure:: display end type contains subroutine display(b) implicit none class(t_B),intent(in):: b print*, b%foo end subroutine end module program main use m_A implicit none type(t_B) a type(t_B) b a%foo=1 b%foo=2 call b%display() call a%display() end program
What is the output when compiled and run?
1 1
2 2
1 2
2 1
Solution
NO: the objects
a
andb
each have separate storage for their own values offoo
NO: same reason as a.
NO: not quite, note the order the functions are called.
YES: when
call b%display()
is executedb%foo
in thedisplay
subroutine takes on the value of 2 and is printed out. Whencall a%display
is executedb%foo
in thedisplay
subroutine takes on the value of ` and is printed out.
Key Points
A type bound procedure allows you to associate a procedure with a type.
Extended types can use these type bound procedures by using the class keyword rather than the type keyword.
select type allows different code paths to execute based on the object’s type similar to select case.
Destructors
Overview
Teaching: 10 min
Exercises: 5 minQuestions
What is a destructor?
Objectives
Create a destructor for our two derived types to deallocate memory.
One aspect I have been ignoring until now is that the memory we allocate for our vectors is never explicitly freed by our program. So far our program has been simple enough that this is not a serious issue. We have only created a few objects that allocate memory within our main program. When the program execution has completed, that memory is returned to the operating system. However, if we had a long running loop inside our program that created new objects with allocated memory and we never deallocated that memory we would have a problem as our program would steadily increase its memory usage. This is referred to as a memory leak as was mentioned in the first half of this workshop. We can manually deallocate memory as we did with allocating memory, however there is a way to create a new special type bound procedure that is automatically called when the object goes out of scope to deallocate this memory for us. To do this we use the final keyword within the type definition.
type <type-name>
...
contains
final:: <type-finalization-subroutine>
end type
Then as we did for the display
subroutine, we can create a <type-finalization-subroutine>
to do anything that needs to be done when an object of this type goes out of scope, including deallocating memory.
Lets add destructors to deallocate our memory for our two derived types.
$ cp type_bound_procedures_select_type.f90 destructor.f90
$ nano destructor.f90
module m_vector
implicit none
type t_vector
integer:: num_elements
real,dimension(:),allocatable:: elements
contains
procedure:: display
final:: destructor_vector
end type
interface t_vector
...
end interface
type,extends(t_vector):: t_vector_3
contains
final:: destructor_vector_3
end type
interface t_vector_3
procedure:: create_size_3_vector
end interface
contains
subroutine destructor_vector(self)
implicit none
type(t_vector):: self
if (allocated(self%elements)) then
deallocate(self%elements)
endif
end subroutine
subroutine destructor_vector_3(self)
implicit none
type(t_vector_3):: self
if (allocated(self%elements)) then
deallocate(self%elements)
endif
end subroutine
subroutine display(vec)
...
end subroutine
type(t_vector) function create_empty_vector()
...
end function
type(t_vector) function create_sized_vector(vec_size)
...
end function
type(t_vector_3) function create_size_3_vector()
...
end function
end module
program main
use m_vector
implicit none
type(t_vector) numbers_none,numbers_some
type(t_vector_3) location
numbers_none=t_vector()
call numbers_none%display()
numbers_some=t_vector(4)
numbers_some%elements(1)=2
call numbers_some%display()
location=t_vector_3()
location%elements(1)=1.0
call location%display()
end program
$ gfortran destructor.f90 -o destructor
$ ./destructor
t_vector:
num_elements= 0
elements=
t_vector:
num_elements= 4
elements=
2.00000000
0.00000000
0.00000000
0.00000000
t_vector_3:
elements=
1.00000000
0.00000000
0.00000000
No allocated check?
What happens if we don’t check that memory is allocated before de-allocating it in our destructor? Lets copy our last code and comment out those lines and see.
$ cp destructor.f90 destructor_no_check.f90 $ nano destructor_no_check.f90
... subroutine destructor_vector(self) implicit none type(t_vector):: self !if(allocated(self%elements)) then deallocate(self%elements) !endif end subroutine subroutine destructor_vector_3(self) implicit none type(t_vector_3):: self !if(allocated(self%elements)) then deallocate(self%elements) !endif end subroutine ...
$ gfortran -g destructor_no_check.f90 -o destructor_no_check $ ./destructor_no_check
What happens and why?
Note: the-g
option provides extra debugging information, such as file and line numbers in the backtrace.Solution
t_vector: num_elements= 0 elements= t_vector: num_elements= 4 elements= 2.00000000 0.00000000 0.00000000 0.00000000 At line 37 of file destructor.f90 Fortran runtime error: Attempt to DEALLOCATE unallocated 'self' Error termination. Backtrace: #0 0x7fd37fc21730 in ??? #1 0x7fd37fc22289 in ??? #2 0x7fd37fc22906 in ??? #3 0x401929 in __m_vector_MOD_destructor_vector at /home/user100/fortran_oop/destructor.f90:37 #4 0x40100d in __m_vector_MOD___final_m_vector_T_vector at /home/user100/fortran_oop/destructor.f90:86 #5 0x401d86 in MAIN__ at /home/user100/fortran_oop/destructor.f90:101 #6 0x401e15 in main at /home/user100/fortran_oop/destructor.f90:89
Key Points
A destructor is used to perform clean up when an object goes out of scope.
To create a destructor use the final keyword when declaring at type bound procedure instead of the procedure keyword.
Operator Overloading
Overview
Teaching: 10 min
Exercises: 5 minQuestions
What is operator overloading?
Objectives
Overload the ‘+’ operator.
Previously in the Interface Blocks episode, we saw how interfaces can be used to map one procedure name onto different underlying procedures based on the number, order, and type or arguments passed to the procedure. However, these interface blocks can be used with more than just procedures they can also be used for operators already defined in Fortran such as +
,-
, *
,/
,==
,/=
, and also operators such as .gt.
, .le.
etc.
Lets add a +
operator which appends one of our t_vector
objects to another creating a new t_vector
.
$ cp destructor.f90 operators.f90
$ nano operators.f90
module m_vector
implicit none
type t_vector
...
end type
interface t_vector
...
end interface
interface operator(+)
procedure:: add_vectors
end interface
type,extends(t_vector):: t_vector_3
...
end type
interface t_vector_3
procedure:: create_size_3_vector
end interface
contains
type(t_vector) function add_vectors(left,right)
implicit none
type(t_vector),intent(in):: left, right
integer:: i,total_size,result_i
total_size=left%num_elements+right%num_elements
add_vectors=create_sized_vector(total_size)
!copy over left vector elements
do i=1, left%num_elements
add_vectors%elements(i)=left%elements(i)
end do
!copy over right vector elements
result_i=left%num_elements+1
do i=1,right%num_elements
add_vectors%elements(result_i)=right%elements(i)
result_i=result_i+1
enddo
end function
subroutine destructor_vector(self)
...
end subroutine
subroutine destructor_vector_3(self)
...
end subroutine
subroutine display(vec)
...
end subroutine
type(t_vector) function create_empty_vector()
...
end function
type(t_vector) function create_sized_vector(vec_size)
...
end function
type(t_vector_3) function create_size_3_vector()
...
end function
end module
program main
use m_vector
implicit none
type(t_vector) numbers_some,numbers_less,numbers_all
numbers_some=t_vector(4)
numbers_some%elements(1)=1
numbers_some%elements(2)=2
numbers_some%elements(3)=3
numbers_some%elements(4)=4
print*, ""
print*, "numbers_some"
call numbers_some%display()
numbers_less=t_vector(2)
numbers_less%elements(1)=5
numbers_less%elements(2)=6
print*, ""
print*, "numbers_less"
call numbers_less%display()
numbers_all=numbers_some+numbers_less
print*, ""
print*, "numbers_all"
call numbers_all%display()
end program
In the main program you can see how the operator is used, exactly as if we were adding two numbers together, however in this case we are adding our two vectors together. Lets try it out.
$ gfortran operators.f90 -o operators
$ ./operators
numbers_some
t_vector:
num_elements= 4
elements=
1.00000000
2.00000000
3.00000000
4.00000000
numbers_less
t_vector:
num_elements= 2
elements=
5.00000000
6.00000000
numbers_all
t_vector:
num_elements= 6
elements=
1.00000000
2.00000000
3.00000000
4.00000000
5.00000000
6.00000000
Here you can see that the two vectors have been combined into numbers_all
with the left hand t_vector
object added first followed by the right hand t_vector
object added second. So the way we have implemented our operator(+)
the results are order dependent.
Which side of an operator?
Our main program above looks like so:
111 program main 112 use m_vector 113 implicit none 114 type(t_vector) numbers_some,numbers_less,numbers_all 115 type(t_vector_3) location 116 117 numbers_some=t_vector(4) 118 numbers_some%elements(1)=1 119 numbers_some%elements(2)=2 120 numbers_some%elements(3)=3 121 numbers_some%elements(4)=4 122 print*, "numbers_some" 123 call numbers_some%display() 124 125 numbers_less=t_vector(2) 126 numbers_less%elements(1)=5 127 numbers_less%elements(2)=6 128 print*, "numbers_less" 129 call numbers_less%display() 130 131 numbers_all=numbers_some+numbers_less 132 print*, "numbers_all" 133 call numbers_all%display() 134 135 end program
Line 131 is
numbers_all=numbers_some+numbers_less
what would line 133,call numbers_all%display()
print out if line 131 wherenumbers_all=numbers_less+numbers_some
instead?
t_vector: num_elements= 6 elements= 1.00000000 2.00000000 3.00000000 4.00000000 5.00000000 6.00000000
the same as before, the
+
operator is commutative, in other words the order doesn’t matter.t_vector: num_elements= 6 elements= 5.00000000 6.00000000 1.00000000 2.00000000 3.00000000 4.00000000
the contents of
numbers_less
has been added to the new vector first followed by the contents ofnumbers_some
.Solution
NO: while it is true that for the mathematical
+
operator the order doesn’t matter this is not true for in general for operators. The implementation of the operator must be written in a way that it is order independent and that is now how we wrote it in this case.YES: In our case the order does matter as we first copy the data in the object left side, passed first as
left
, then the object on the right, passed second asright
to the function.
Key Points
Common Fortran operators can be overloaded to work with your custom derived types.
Wrapping Up
Overview
Teaching: 5 min
Exercises: 0 minQuestions
Where can I learn more about Fotran?
Objectives
More information:
- www.ace-net.ca (additional training, staff expertise, etc.)
- How can you reach us: info@ace-net.ca, support@ace-net.ca
Fortran References:
- http://www.icl.utk.edu/~mgates3/docs/fortran.html (one guy’s Fortran90/95 cheat sheet)
- https://courses.physics.illinois.edu/phys466/sp2013/comp_info/array.html (array operations)
- https://www.admin-magazine.com/HPC/Articles/Modern-Fortran-Part-1
- https://www.admin-magazine.com/HPC/Articles/Modern-Fortran-Part-2
- http://armnlib.uqam.ca/PDF/john_reid_new_fortran_2003.pdf (38-page paper from the ISO committee lead on F95-to-F2003 changes)
- https://gcc.gnu.org/onlinedocs/ (GNU manuals)
- https://gcc.gnu.org/onlinedocs/gcc-11.3.0/gfortran/
- https://www.intel.com/content/www/us/en/develop/documentation/fortran-compiler-oneapi-dev-guide-and-reference/top.html (Intel Fortran manuals)
- https://fortranwiki.org/fortran/show/HomePage (a Fortran wiki !?)
- https://www.tutorialspoint.com/fortran/index.htm
- https://fortran-lang.org/
Key Points