Procedures again: interfaces
Overview
Teaching: 10 min
Exercises: 30 minQuestions
What is an interface and why might I need to write one?
Can I use a function or a subroutine as an argument to another procedure?
How can I write a procedure that will accept arguments of different types?
Can I overload arithmetic and relational operators to work with my derived types?
Can I write a function that will accept both scalar and array arguments?
Objectives
Understand the need for interfaces and recognise when they are necessary.
Be able to write an interface to allow the passing of a procedure as an actual argument.
Understand and be able to implement overloading and polymorphism.
Understand what an
elemental
function is and be able to write one.
So far, we have demanded that all procedures be defined as part of a
module. This has the important advantage that it makes the
interface to the function or subroutine explicit to the compiler,
and to other program units via the use
statement. The compiler can
then check the order and type of the actual arguments.
Other situations arise where we may need to give the compiler additional information about the interface.
An external function
Consider a program unit (which may or may not be a separate file) which contains a function declaration outside a module scope. E.g.,
function my_mapping(i) result(imap)
integer, intent(in) :: i
integer :: imap
...
end function my_mapping
If we compile a program which includes a reference to this function, an error may result. We have not given the compiler any information about how the function is meant to be called.
It is possible to provide the compiler with some limited information
about the return value of the function with the external
attribute
(also available as a statement). E.g.,
program example
implicit none
integer, external :: my_mapping
...
However, we have still not given the compiler full information about
the interface. The interface is said to remain implicit. To make
it explicit, the interface
construct is available:
program example
implicit none
interface
function my_mapping(i) result(imap)
integer, intent(in) :: i
integer :: imap
end function my_mapping
end interface
...
This has provided a full, explicit, statement about the interface of the
function my_mappping()
. (Note that the dummy argument names are not
significant, but the function name and the argument types and intents are.)
The compiler should now be able to check the arguments are used correctly in the calling program (as if we had declared the function in a module).
Interface blocks are necessary in other contexts.
Exercise (5 minutes)
Externals and interfaces
To illustrate the points made above, a very simple external function is defined in the file external.f90, and an accompanying program example1.f90 calls the function therein.
Try to compile the two files, e.g.:
$ ftn external.f90 example1.f90
What happens?
Try adding the appropriate
external
declarationinteger, external :: array_size
What happens if you try to compile the program now? (Note at this point that there are no modules involved, so no
.mod
files will appear).Then try running the program. Is the output what you expect?
Finally, remove the
external
declaration and try to introduce the correctinterface
block. What happens now?Solution
Without either the
external
declaration of the function or an interface, the compilation will simply fail.gfortran
produces the following:example1.f90:24:33: 24 | print *, "The array size is: ", array_size(a), size(a) | 1 Error: Function 'array_size' at (1) has no IMPLICIT type
Adding the
external
declaration allows compilation to succeed, but the following output is produced on running the program:The array size is: 0 6
The function call returns an incorrect value of 0 where the (3,2) array has an actual size of 6.
Removing the external declaration and adding an interface matching the function in
external.f90
as follows will help (and you may have already spotted the issue):interface function array_size(a) result(isize) real, dimension(:), intent(in) :: a integer :: isize end function array_size end interface
Compiling now produces an error again, as reported here by
gfortran
:example1.f90:23:45: 23 | print *, "The array size is: ", array_size(a), size(a) | 1 Error: Rank mismatch in argument 'a' at (1) (rank-1 and rank-2)
In the main program we were attempting call
array_size()
on a rank two array, while inexternal.f90
we implemented it for rank one arrays. Now that we have an interface, the compiler can see that the way we’re calling the function is incorrect, so it produces the error and fails compilation.This is preferable to the
external
declaration by which we essentially asked the compiler to trust that it can callarray_size()
however we tell it to do so, even though it was in this case incorrect.
Passing functions or subroutines as arguments
Sometimes it is convenient to provide a procedure as an argument to another function or subroutine. Good examples of this are for numerical optimisation or numerical integration methods where a user-defined function needs to be evaluated for a series of different arguments which cannot be prescribed in advance.
This can be done if an interface block is provided which describes to the calling procedure the function that is the dummy argument. E.g.,
subroutine my_integral(a, b, afunc, result)
real, intent(in) :: a
real, intent(in) :: b
interface
function afunc(x) result(y)
real, intent(in) :: x
real :: y
end function afunc
end interface
...
The function dummy argument has no intent. One cannot in general use intrinsic procedures as actual arguments.
Limited polymorphism
We have seen a number of intrinsic functions which take arguments of
different types, such as mod()
. Such a situation where the arguments
can take a different form is sometimes referred to as limited polymorphism
(or overloading).
However, it is not possible in Fortran to define two procedures of the same name, but different arguments (at least in the same scope). We need different names; suppose we have two module sub-programs, schematically:
subroutine my_specific_int(ia)
integer, intent(inout) :: ia
... integer implementation ...
end subroutine my_specific_int
subroutine my_specfic_real(ra)
real, intent(inout) :: ra
... real implementation ...
end subroutine my_specific_real
A mechanism exists to allow the compiler to identify the correct routine based on the actual argument when used with a generic name. This is:
interface my_generic_name
module procedure my_specific_int
module procedure my_specific_real
...
end interface my_generic_name
This should appear in the specification (upper) part of the relevant module. The two specific implementations must be distinguishable by the compiler, that is, at least one non-optional dummy argument must be different.
Exercise
An interface for polymorphic PBMs
In the earlier episode on I/O, we wrote a module to produce a
.pbm
image file. The accompanying module pbm_image.f90 provides two implementations of such a routine: one for a logical array, and another for an integer array.Check you can add the appropriate
interface
block with the generic namewrite_pbm
to allow the program example3.f90 to be compiled correctly.Solution
We need to add an interface to the module specification for
write_pbm
which allows use of both thewrite_logical_pbm
andwrite_integer_pbm
subroutines. You should be able to follow the description above to do so. The only minor complication is that the module usesprivate
by default; unless we specifypublic
for the new generic interface, it will remain hidden from the main program.Putting this together, the following should allow you to compile and run the program with no errors:
public :: write_pbm interface write_pbm module procedure write_logical_pbm module procedure write_integer_pbm end interface
With this, the internal workings of the module are hidden away and its use is entirely via
write_pbm
.
Operator overloading
For simple derived types it may be meaningful to define relational and arithmetic operators. For example, if we had a date type such as
type :: my_date
integer :: day
integer :: month
integer :: year
end type my_date
it may be meaningful to ask whether two dates are equal and so on (it would not really be meaningful to add one date to another).
One can write a function to do this:
function my_dates_equal(date1, date2) result(equal)
type (my_date), intent(in) :: date1
type (my_date), intent(in) :: date2
logical equal
! ...
end function my_dates_equal
As a syntactic convenience, it might be useful to use ==
in a logical
expression using dates. This can be arranged via
interface operator(==)
module procedure my_dates_equal
end interface
Again this should appear in the relevant specification part of the
relevant module. Such overloading is possible for relational operators
==
, /=
, >=
, <=
, >
and <
. If appropriate, overloading is also
available for arithmetic operators +
, -
, *
, and /
.
It is also possible to overload assignment =
.
Elemental functions
Again, we have seen that some intrinsic functions allow either scalar or array actual arguments. The same effect can be achieved for a user-defined function by declaring it to be elemental. The procedure is declared in terms of a scalar dummy argument, but then may be applied to an array actual argument element by element.
Such a procedure should be declared:
elemental function my_function(a) result(b)
integer, intent(in) :: a
integer :: b
! ...
end function my_function
An invocation should be, e.g.:
iresult(1:4) = my_function(ival(1:4))
All arguments (and function results) must conform. An elemental routine
usually must also be pure
.
Exercise (5 minutes)
Elemental conversion from logical to PBM
In the pbm_image.f90 module there is a utility function
logical_to_pbm()
which is used inwrite_logical_pbm()
to translate the logical array to an integer array. Refactor this part of the code to use an elemental function.Solution
The function
logical_to_pbm()
is currentlypure
and takes in a scalar logicallvar
to return the scalar integerivar
. The function is currentlypure
. Everything is already in place to convert the function toelemental
:elemental function logical_to_pbm(lvar) result (ivar) ! Utility to return 0 or 1 for .false. and .true. logical, intent(in) :: lvar integer :: ivar ivar = 0 if (lvar) ivar = 1 end function logical_to_pb
All that remains is to have
write_logical_pbm()
call this newelemental
version. Further down in that function you will see the old nested loop to move through themap
array and from it uselogical_to_pbm()
to fill theimap
array:do j = 1, size(map, dim = 2) do i = 1, size(map, dim = 1) imap(i,j) = logical_to_pbm(map(i,j)) end do end do
With the new
elemental
version oflogical_to_pbm()
, we can replace this entire structure with the single line:imap(:,:) = logical_to_pbm(map(:,:))
Exercise (20 minutes)
Exercise name
Write a module/program to perform a very simple numerical integration of a simple one-dimensional function f(x). We can use a trapezoidal rule: for lower and upper limits a and b, the integral can be approximated by
(b - a)*(f(a) + f(b))/2.0
We can go further and split the interval between
a
andb
into small sections of sizeh = (b - a)/n
. In a similar manner, approximating the integral of each small section with a trapezium allows us to estimate the total integral to beh*(f(a) + sum + f(b))/2.0
with the sum
sum = 0.0 do k = 1, n-1 sum = sum + 2.0*f(a+k*h) end do
Write a procedure that will take the limits
a
andb
, the integer number of stepsn
, and the function, and returns a result.To check, you can evaluate the function
cos(x) sin(x)
betweena = 0
andb = pi/2
(the answer should be 1/2). Check your answer gets better for value ofn = 10, 100, 1000
.Solution
A sample solution is provided in integral_program.f90 and integral_module.f90.
Key Points
An interface provides important information about a procedure to the compiler. Without this information, the compiler may not be able to use it.
When using modules, interfaces are generated automatically and provided to the compiler.
If not in a module, a function can be made visible to the compiler by declaring it with the
external
attribute, but the compiler can go further by checking the argument types if you write a full, explicitinterface
block.Interfaces can also be used to implement limited polymorphism and operator overloading for derived types.
An elemental function is one which can be applied to a scalar actual argument or element-by-element to an array actual argument.