Object Oriented Programming with Fortran

Introduction

Overview

Teaching: 5 min
Exercises: 0 min
Questions
  • 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:

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.

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 min
Questions
  • 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

modules.f90

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:

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

modules_access_none.f90

Add the private access modifier

Copy the modules.f90 file to modules_access_none.f90 and add the private access modifier to the m_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 and value_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 and value_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

modules_access_some.f90

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 min
Questions
  • 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

derived_types.f90

$ 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

derived_types_init.f90

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 the elements member variable can be.

What is a derived type?

Is a derived type:

  1. a variable which can have multiple variable members

  2. an object which can have multiple member variables

  3. a datatype which can have multiple member variables

  4. a datatype which can contain only a single value like integer,real,etc.

Solution

  1. 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.

  2. NO: while an object has a derived type, the object is not the derived type its self.

  3. Yes: a derived type is a kind of datatype that can have multiple members.

  4. 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 min
Questions
  • 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

type_extension.f90

$ 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
...
  1. A

  2. B

  3. C

  4. D

Solution

  1. No: close, but A is the new derived type which extends the existing B derived type.

  2. Yes: the existing derived type B is being extended to create a new derived type A.

  3. 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.

  4. 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 min
Questions
  • 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

interface_blocks.f90

$ 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 the t_vector generic interface? Make a copy of our last source file and add the line procedure:: create_size_3_vector to the t_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 the create_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: min
Questions
Objectives

Key Points


Type Bound Procedures

Overview

Teaching: 10 min
Exercises: 5 min
Questions
  • 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

type_bound_procedures.f90

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

type_bound_procedures_select_type.f90

$ 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
    1
    
  2. 2
    2
    
  3. 1
    2
    
  4. 2
    1
    

Solution

  1. NO: the objects a and b each have separate storage for their own values of foo

  2. NO: same reason as a.

  3. NO: not quite, note the order the functions are called.

  4. YES: when call b%display() is executed b%foo in the display subroutine takes on the value of 2 and is printed out. When call a%display is executed b%foo in the display 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 min
Questions
  • 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

destructor.f90

$ 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 min
Questions
  • 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

operators.f90

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 where numbers_all=numbers_less+numbers_some instead?

  1. 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.

  2. 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 of numbers_some.

Solution

  1. 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.

  2. 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 as right 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 min
Questions
  • Where can I learn more about Fotran?

Objectives

More information:

Fortran References:

Key Points